diff --git a/.github/actions/prepare/action.yml b/.github/actions/prepare/action.yml new file mode 100644 index 00000000..033c0ebc --- /dev/null +++ b/.github/actions/prepare/action.yml @@ -0,0 +1,24 @@ +name: Prepare +description: Prepare CI environment + +runs: + using: composite + steps: + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: "3.x" + channel: "stable" + - name: Download sqlite3.wasm + uses: actions/download-artifact@v4 + with: + name: sqlite3-wasm + path: packages/sqlite3_wasm_build/dist/ + - name: Install Melos + shell: sh + run: flutter pub global activate melos + - name: Install dependencies + shell: sh + env: + IS_IN_POWERSYNC_CI: 1 + run: melos prepare diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 00000000..b0450c8c --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,61 @@ +name: Check packages and demos + +concurrency: + group: packages-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + +on: + push: + branches: + - "**" + +jobs: + setup: + uses: ./.github/workflows/prepare_wasm.yml + + build: + runs-on: ubuntu-latest + needs: [setup] + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - name: Check formatting + run: melos format:check:packages + - name: Lint + run: melos analyze:packages + - name: Publish dry-run + run: melos publish --dry-run --yes + + pana: + runs-on: ubuntu-latest + needs: [setup] + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - name: Check pana score + run: | + flutter pub global activate pana + melos analyze:packages:pana --no-select + + test: + runs-on: ubuntu-latest + needs: [setup] + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - name: Run flutter tests + run: melos test + - name: Run dart tests + run: melos test:web + + check_demos: + runs-on: ubuntu-latest + needs: [setup] + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - name: Check formatting + run: melos format:check:demos + - name: Lint + run: | + ./.github/workflows/scripts/copy-config.sh + melos analyze:demos diff --git a/.github/workflows/demos.yml b/.github/workflows/demos.yml deleted file mode 100644 index 1a83c277..00000000 --- a/.github/workflows/demos.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Demos checks - -concurrency: - group: demos-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - -on: - push: - branches: - - "**" - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Install Flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: "3.x" - channel: "stable" - - - name: Install Melos - run: flutter pub global activate melos - - name: Install dependencies - run: melos prepare - - name: Check formatting - run: melos format:check:demos - - name: Lint - run: | - ./.github/workflows/scripts/copy-config.sh - melos analyze:demos diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml deleted file mode 100644 index 4b8537c3..00000000 --- a/.github/workflows/packages.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: Packages check - -concurrency: - group: packages-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - -on: - push: - branches: - - "**" - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Install Flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: "3.x" - channel: "stable" - - - name: Install Melos - run: flutter pub global activate melos - - name: Install dependencies - run: melos prepare - - name: Check formatting - run: melos format:check:packages - - name: Lint - run: melos analyze:packages - - name: Publish dry-run - run: melos publish --dry-run --yes - - pana: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Install Flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: "3.x" - channel: "stable" - - - name: Install Melos - run: flutter pub global activate melos - - name: Install dependencies - run: melos prepare - - name: Check pana score - run: | - flutter pub global activate pana - melos analyze:packages:pana --no-select - - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Install Flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: "3.x" - channel: "stable" - - name: Install melos - run: flutter pub global activate melos - - name: Install dependencies and prepare project - run: melos prepare - - name: Run flutter tests - run: melos test - - name: Run dart tests - run: melos test:web diff --git a/.github/workflows/prepare_wasm.yml b/.github/workflows/prepare_wasm.yml new file mode 100644 index 00000000..5da977c1 --- /dev/null +++ b/.github/workflows/prepare_wasm.yml @@ -0,0 +1,53 @@ +name: Build SQLite3 WASM + +on: + workflow_call: + +jobs: + compile_sqlite3_wasm: + name: Compile sqlite3 wasm + runs-on: macos-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + id: cache_build + with: + path: packages/sqlite3_wasm_build/dist/ + key: wasm-${{ runner.os }}-${{ hashFiles('packages/sqlite3_wasm_build/build.sh') }} + + - name: Setup Homebrew + if: steps.cache_build.outputs.cache-hit != 'true' + id: set-up-homebrew + uses: Homebrew/actions/setup-homebrew@master + - name: Install Dart SDK + if: steps.cache_build.outputs.cache-hit != 'true' + uses: dart-lang/setup-dart@v1 + - name: Setup macOS build dependencies + if: steps.cache_build.outputs.cache-hit != 'true' + run: brew install llvm lld binaryen wasi-libc wasi-runtimes + - name: Compile sqlite3.wasm on macOS + if: steps.cache_build.outputs.cache-hit != 'true' + working-directory: packages/sqlite3_wasm_build + run: ./build.sh + + - name: Upload built sqlite3 binaries + uses: actions/upload-artifact@v4 + with: + name: sqlite3-wasm + path: packages/sqlite3_wasm_build/dist/ + if-no-files-found: error + retention-days: 1 + + - name: Setup Node.js + uses: actions/setup-node@v4 + - uses: pnpm/action-setup@v2 + name: Install pnpm + with: + run_install: false + version: 10 + - name: Dry-run npm publish + working-directory: packages/sqlite3_wasm_build + run: | + pnpm i + pnpm publish --no-git-checks --dry-run diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fb3a0ec4..6a63085c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,6 +5,8 @@ on: - "powersync-v[0-9]+.[0-9]+.[0-9]+" - "powersync_attachments_helper-v[0-9]+.[0-9]+.[0-9]+*" - "powersync_flutter_libs-v[0-9]+.[0-9]+.[0-9]+*" + - "powersync_core-v[0-9]+.[0-9]+.[0-9]+*" + - "powersync_sqlcipher-v[0-9]+.[0-9]+.[0-9]+*" workflow_dispatch: jobs: @@ -35,7 +37,7 @@ jobs: env: GITHUBREF: ${{ github.ref }} run: | - PACKAGE_NAME=$(sed -E 's/refs\/tags\/([a-z0-9_]+)-v([0-9]+\.[0-9]+\.[0-9]+)/\1/' <<< $GITHUBREF) && \ + PACKAGE_NAME=$(sed -E 's/refs\/tags\/([a-z0-9_]+)-v([0-9]+\.[0-9]+\.[0-9]+(\+[0-9]+)?)/\1/' <<< $GITHUBREF) && \ echo "PACKAGE_NAME=$PACKAGE_NAME" >> $GITHUB_ENV echo "Package name: $PACKAGE_NAME" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3231dca0..0adbcd81 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,6 +7,9 @@ on: - 'powersync-v[0-9]+.[0-9]+.[0-9]+' jobs: + setup: + uses: ./.github/workflows/prepare_wasm.yml + build: runs-on: ubuntu-latest permissions: @@ -15,18 +18,7 @@ jobs: steps: - name: Checkout Repository uses: actions/checkout@v4 - - - name: Install Flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: '3.x' - channel: 'stable' - - - name: Install Melos - run: flutter pub global activate melos - - - name: Install Dependencies and Compile Assets - run: melos prepare + - uses: ./.github/actions/prepare - name: Create Draft Release env: @@ -39,4 +31,24 @@ jobs: body="Release $tag $changes" gh release create "$tag" --title "$tag" --notes "$body" - gh release upload "${{ github.ref_name }}" packages/powersync/assets/powersync_db.worker.js packages/powersync/assets/powersync_sync.worker.js + gh release upload "${{ github.ref_name }}" packages/powersync/assets/powersync_db.worker.js packages/powersync/assets/powersync_sync.worker.js packages/sqlite3_wasm_build/dist/*.wasm + + - name: Setup Node.js + uses: actions/setup-node@v4 + - uses: pnpm/action-setup@v2 + name: Install pnpm + with: + run_install: false + version: 10 + - name: Add NPM auth + run: | + echo "//registry.npmjs.org/:_authToken=${{secrets.NPM_TOKEN}}" >> ~/.npmrc + - name: Publish npm package with WASM files + working-directory: packages/sqlite3_wasm_build + run: | + pnpm i + npm version --allow-same-version --no-git-tag-version $(echo $GITHUB_REF_NAME | sed -E 's/powersync-v//') + pnpm publish --no-git-checks --access public + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/scripts/copy-config.sh b/.github/workflows/scripts/copy-config.sh index 21457486..f2c0a00d 100755 --- a/.github/workflows/scripts/copy-config.sh +++ b/.github/workflows/scripts/copy-config.sh @@ -19,6 +19,9 @@ copy_config_files() { echo "Copied contents of $template_config to ${template_config%/*}/$TARGET_CONFIG_FILE" fi done + + # Create a new file .env for supabase-trello demo + echo -n > demos/supabase-trello/.env } # Call the function for the single demos folder diff --git a/.gitignore b/.gitignore index 1baa751f..95fba1e8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ pubspec_overrides.yaml .flutter-plugins-dependencies .flutter-plugins build +**/doc/api +.build # Shared assets assets/* @@ -19,6 +21,7 @@ assets/* powersync_db.worker.js powersync_db.worker.js* sqlite3.wasm +sqlite3mc.wasm powersync_sync.worker.js powersync_sync.worker.js* diff --git a/CHANGELOG.md b/CHANGELOG.md index c49add44..e76e3a6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,744 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## 2025-10-06 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`powersync` - `v1.16.1`](#powersync---v1161) + - [`powersync_core` - `v1.6.1`](#powersync_core---v161) + - [`powersync_sqlcipher` - `v0.1.13`](#powersync_sqlcipher---v0113) + +--- + +#### `powersync` - `v1.16.1` + + - Web: Fix decoding sync streams on status. + +#### `powersync_core` - `v1.6.1` + + - Web: Fix decoding sync streams on status. + + - **DOCS**: Point to uses in example. ([4f4da24e](https://github.com/powersync-ja/powersync.dart/commit/4f4da24e580dec6b1d29a5e0907b83ba7c55e3d8)) + +#### `powersync_sqlcipher` - `v0.1.13` + + - Web: Fix decoding sync streams on status. + + +## 2025-10-02 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`powersync_attachments_helper` - `v0.6.20`](#powersync_attachments_helper---v0620) + - [`powersync` - `v1.16.0`](#powersync---v1160) + - [`powersync_core` - `v1.6.0`](#powersync_core---v160) + - [`powersync_flutter_libs` - `v0.4.12`](#powersync_flutter_libs---v0412) + - [`powersync_sqlcipher` - `v0.1.12`](#powersync_sqlcipher---v0112) + +--- + +#### `powersync_attachments_helper` - `v0.6.20` + + - Add note about new attachment queue system in core package. + +#### `powersync` - `v1.16.0` +#### `powersync_core` - `v1.6.0` +#### `powersync_sqlcipher` - `v0.1.12` + +- Add `getCrudTransactions()` returning a stream of completed transactions for uploads. +- Add experimental support for [sync streams](https://docs.powersync.com/usage/sync-streams). +- Add new attachments helper implementation in `package:powersync_core/attachments/attachments.dart`. +- Add SwiftPM support. +- Add support for compiling `powersync_core` with `build_web_compilers`. + +#### `powersync_flutter_libs` - `v0.4.12` + + - Update core extension. + - Add support for SwiftPM. + +## 2025-08-18 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`powersync_attachments_helper` - `v0.6.19`](#powersync_attachments_helper---v0619) + +--- + +#### `powersync_attachments_helper` - `v0.6.19` + + - Remove direct dependency on `sqlite_async`. + + +## 2025-08-14 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`powersync_core` - `v1.5.2`](#powersync_core---v152) + - [`powersync` - `v1.15.2`](#powersync---v1152) + - [`powersync_sqlcipher` - `v0.1.11+1`](#powersync_sqlcipher---v01111) + +--- + +#### `powersync_core` - `v1.5.2` + + - Fix excessive memory consumption during large sync. + +#### `powersync` - `v1.15.2` + + - Fix excessive memory consumption during large sync. + +#### `powersync_sqlcipher` - `v0.1.11+1` + + - Fix excessive memory consumption during large sync. + + +## 2025-08-11 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`powersync_core` - `v1.5.1`](#powersync_core---v151) + - [`powersync` - `v1.15.1`](#powersync---v1151) + - [`powersync_sqlcipher` - `v0.1.11`](#powersync_sqlcipher---v0111) + - [`powersync_flutter_libs` - `v0.4.11`](#powersync_flutter_libs---v0411) + +--- + +#### `powersync_core` - `v1.5.1` +#### `powersync` - `v1.15.1` +#### `powersync_sqlcipher` - `v0.1.11` +#### `powersync_flutter_libs` - `v0.4.11` + + - Support latest versions of `package:sqlite3` and `package:sqlite_async`. + - Stream client: Improve `disconnect()` while a connection is being opened. + - Stream client: Support binary sync lines with Rust client and compatible PowerSync service versions. + - Sync client: Improve parsing error responses. + +## 2025-07-17 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`powersync_core` - `v1.5.0`](#powersync_core---v150) + - [`powersync` - `v1.15.0`](#powersync---v1150) + - [`powersync_sqlcipher` - `v0.1.10`](#powersync_sqlcipher---v0110) + - [`powersync_flutter_libs` - `v0.4.10`](#powersync_flutter_libs---v0410) + - [`powersync_attachments_helper` - `v0.6.18+11`](#powersync_attachments_helper---v061811) + +Packages with dependency updates only: + +> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project. + + - `powersync_attachments_helper` - `v0.6.18+11` + +--- + +#### `powersync_flutter_libs` - `v0.4.10`. + + - Update the PowerSync core extension to `0.4.2`. + +#### `powersync_core` - `v1.5.0` +#### `powersync` - `v1.15.0` +#### `powersync_sqlcipher` - `v0.1.10` + + - Add support for [raw tables](https://docs.powersync.com/usage/use-case-examples/raw-tables), which are user-managed + regular SQLite tables instead of the JSON-based views managed by PowerSync. + + +## 2025-07-07 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`powersync_core` - `v1.4.1`](#powersync_core---v141) + - [`powersync` - `v1.14.1`](#powersync---v1141) + - [`powersync_sqlcipher` - `v0.1.9`](#powersync_sqlcipher---v019) + - [`powersync_attachments_helper` - `v0.6.18+10`](#powersync_attachments_helper---v061810) + +Packages with dependency updates only: + +> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project. + + - `powersync_attachments_helper` - `v0.6.18+10` + +--- + +#### `powersync_core` - `v1.4.1` +#### `powersync` - `v1.14.1` +#### `powersync_sqlcipher` - `v0.1.9` + + - Rust client: Fix uploading local writes after reconnect. + - `PowerSyncDatabase.withDatabase`: Rename `loggers` parameter to `logger` for consistency. + - Fix parsing HTTP errors for sync service unavailability. + +## 2025-06-19 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`powersync_core` - `v1.4.0`](#powersync_core---v140) + - [`powersync` - `v1.14.0`](#powersync---v1140) + - [`powersync_sqlcipher` - `v0.1.8`](#powersync_sqlcipher---v018) + - [`powersync_attachments_helper` - `v0.6.18+9`](#powersync_attachments_helper---v06189) + +Packages with dependency updates only: + +> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project. + + - `powersync_attachments_helper` - `v0.6.18+9` + +--- + +#### `powersync_core` - `v1.4.0` + +#### `powersync` - `v1.14.0` + +#### `powersync_sqlcipher` - `v0.1.8` + +Add a new sync client implementation written in Rust instead of Dart. While +this client is still experimental, we intend to make it the default in the +future. The main benefit of this client is faster sync performance, but +upcoming features will also require this client. +We encourage interested users to try it out by passing `SyncOptions` to the +`connect` method: + +```dart +database.connect( + connector: YourConnector(), + options: const SyncOptions( + syncImplementation: SyncClientImplementation.rust, + ), +); +``` + +Switching between the clients can be done at any time without compatibility +issues. If you run into issues with the new client, please reach out to us! + +#### `powersync_flutter_libs` - `v0.4.9` + + - Update PowerSync core extension to version 0.4.0. + +## 2025-05-29 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`powersync_core` - `v1.3.1`](#powersync_core---v131) + - [`powersync` - `v1.13.1`](#powersync---v1131) + - [`powersync_sqlcipher` - `v0.1.7`](#powersync_sqlcipher---v017) + - [`powersync_attachments_helper` - `v0.6.18+8`](#powersync_attachments_helper---v06188) + +Packages with dependency updates only: + +> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project. + + - `powersync_attachments_helper` - `v0.6.18+8` + +--- + +#### `powersync_core` - `v1.3.1` + +- Use `package:http` instead of `package:fetch_client` on the web (since the former now uses fetch as well). +- Allow disconnecting in the credentials callback of a connector. +- Deprecate retry and CRUD upload durations as fields and independent parameters. Use the new `SyncOptions` class instead. +- Fix sync progress report after a compaction or defragmentation on the sync service. + +#### `powersync` - `v1.13.1` + +- Use `package:http` instead of `package:fetch_client` on the web (since the former now uses fetch as well). +- Allow disconnecting in the credentials callback of a connector. +- Deprecate retry and CRUD upload durations as fields and independent parameters. Use the new `SyncOptions` class instead. +- Fix sync progress report after a compaction or defragmentation on the sync service. + +#### `powersync_sqlcipher` - `v0.1.7` + + - Allow subclassing open factory for SQLCipher. + + +## 2025-05-07 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`powersync_core` - `v1.3.0`](#powersync_core---v130) + - [`powersync` - `v1.13.0`](#powersync---v1130) + - [`powersync_sqlcipher` - `v0.1.6`](#powersync_sqlcipher---v016) + - [`powersync_flutter_libs` - `v0.4.8`](#powersync_flutter_libs---v048) + - [`powersync_attachments_helper` - `v0.6.18+7`](#powersync_attachments_helper---v06187) + +Packages with dependency updates only: + +> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project. + + - `powersync_attachments_helper` - `v0.6.18+7` + +--- + +#### `powersync_core` - `v1.3.0` +#### `powersync` - `v1.13.0` +#### `powersync_sqlcipher` - `v0.1.6` + +* Report real-time progress information about downloads through `SyncStatus.downloadProgress`. +* Add `trackPreviousValues` option on `Table` which sets `CrudEntry.previousValues` to previous values on updates. +* Add `trackMetadata` option on `Table` which adds a `_metadata` column that can be used for updates. + The configured metadata is available through `CrudEntry.metadata`. +* Add `ignoreEmptyUpdates` option which skips creating CRUD entries for updates that don't change any values. + +#### `powersync_flutter_libs` - `v0.4.8` + + - Update PowerSync core extension to version 0.3.14. + + +## 2025-04-24 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`powersync_core` - `v1.2.4`](#powersync_core---v124) + - [`powersync_attachments_helper` - `v0.6.18+6`](#powersync_attachments_helper---v06186) + - [`powersync_sqlcipher` - `v0.1.5+4`](#powersync_sqlcipher---v0154) + - [`powersync` - `v1.12.4`](#powersync---v1124) + +Packages with dependency updates only: + +> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project. + + - `powersync_attachments_helper` - `v0.6.18+6` + - `powersync_sqlcipher` - `v0.1.5+4` + - `powersync` - `v1.12.4` + +--- + +#### `powersync_core` - `v1.2.4` + + - Fix deadlock when `connect()` is called immediately after opening a database. + + +## 2025-04-22 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`powersync_core` - `v1.2.3`](#powersync_core---v123) + - [`powersync_attachments_helper` - `v0.6.18+5`](#powersync_attachments_helper---v06185) + - [`powersync_sqlcipher` - `v0.1.5+3`](#powersync_sqlcipher---v0153) + - [`powersync` - `v1.12.3`](#powersync---v1123) + +Packages with dependency updates only: + +> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project. + + - `powersync_attachments_helper` - `v0.6.18+5` + - `powersync_sqlcipher` - `v0.1.5+3` + - `powersync` - `v1.12.3` + +--- + +#### `powersync_core` - `v1.2.3` + + - Introduce locks to avoid duplicate sync streams when multiple instances of the same database are opened. + - Refactor connect / disconnect internally. + - Warn when multiple instances of the same database are opened. + - Fix race condition causing data not to be applied while an upload is in progress. + - Web: Fix token invalidation logic when a sync worker is used. + + +## 2025-03-11 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`powersync_core` - `v1.2.2`](#powersync_core---v122) + - [`powersync_attachments_helper` - `v0.6.18+4`](#powersync_attachments_helper---v06184) + - [`powersync_sqlcipher` - `v0.1.5+2`](#powersync_sqlcipher---v0152) + - [`powersync` - `v1.12.2`](#powersync---v1122) + +Packages with dependency updates only: + +> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project. + + - `powersync_attachments_helper` - `v0.6.18+4` + - `powersync_sqlcipher` - `v0.1.5+2` + - `powersync` - `v1.12.2` + +--- + +#### `powersync_core` - `v1.2.2` + + - Fix handling token invalidation on the web. + + +## 2025-03-06 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`powersync_core` - `v1.2.1`](#powersync_core---v121) + - [`powersync_flutter_libs` - `v0.4.7`](#powersync_flutter_libs---v047) + - [`powersync_attachments_helper` - `v0.6.18+3`](#powersync_attachments_helper---v06183) + - [`powersync_sqlcipher` - `v0.1.5+1`](#powersync_sqlcipher---v0151) + - [`powersync` - `v1.12.1`](#powersync---v1121) + +Packages with dependency updates only: + +> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project. + + - `powersync_attachments_helper` - `v0.6.18+3` + - `powersync_sqlcipher` - `v0.1.5+1` + - `powersync` - `v1.12.1` + +--- + +#### `powersync_core` - `v1.2.1` + + - Raise minimum version of core extension to 0.3.11. + +#### `powersync_flutter_libs` - `v0.4.7` + + - Update core extension to 0.3.12. + + +## 2025-03-03 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`powersync_core` - `v1.2.0`](#powersync_core---v120) + - [`powersync_flutter_libs` - `v0.4.6`](#powersync_flutter_libs---v046) + - [`powersync` - `v1.12.0`](#powersync---v1120) + - [`powersync_sqlcipher` - `v0.1.5`](#powersync_sqlcipher---v015) + - [`powersync_attachments_helper` - `v0.6.18+2`](#powersync_attachments_helper---v06182) + +Packages with dependency updates only: + +> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project. + + - `powersync_attachments_helper` - `v0.6.18+2` + +--- + +#### `powersync_core` - `v1.2.0` + + - Support bucket priorities and partial syncs. + +#### `powersync_flutter_libs` - `v0.4.6` + + - Bump version of core extension to 0.3.11 + +#### `powersync` - `v1.12.0` + + - Support bucket priorities and partial syncs. + +#### `powersync_sqlcipher` - `v0.1.5` + + - Support bucket priorities and partial syncs. + + +## 2025-02-17 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`powersync_core` - `v1.1.3`](#powersync_core---v113) + - [`powersync_flutter_libs` - `v0.4.5`](#powersync_flutter_libs---v045) + - [`powersync_attachments_helper` - `v0.6.18+1`](#powersync_attachments_helper---v06181) + - [`powersync_sqlcipher` - `v0.1.4+1`](#powersync_sqlcipher---v0141) + - [`powersync` - `v1.11.3`](#powersync---v1113) + +Packages with dependency updates only: + +> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project. + + - `powersync_attachments_helper` - `v0.6.18+1` + - `powersync_sqlcipher` - `v0.1.4+1` + - `powersync` - `v1.11.3` + +--- + +#### `powersync_core` - `v1.1.3` + + - Add explicit casts in sync service, avoiding possible issues with dart2js optimizations. + +#### `powersync_flutter_libs` - `v0.4.5` + + - Update core extension to 0.3.10 in preparation for bucket priorities. + + +## 2025-01-28 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`powersync_core` - `v1.1.2`](#powersync_core---v112) + - [`powersync_attachments_helper` - `v0.6.18`](#powersync_attachments_helper---v0618) + - [`powersync_sqlcipher` - `v0.1.4`](#powersync_sqlcipher---v014) + - [`powersync` - `v1.11.2`](#powersync---v1112) + +Packages with dependency updates only: + +> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project. + + - `powersync_attachments_helper` - `v0.6.18` + - `powersync_sqlcipher` - `v0.1.4` + - `powersync` - `v1.11.2` + +--- + +#### `powersync_core` - `v1.1.2` + + - Web: Support running in contexts where web workers are unavailable. + - Web: Fix sync worker logs not being disabled. + - `powersync_sqlcipher`: Web support. + + +## 2025-01-16 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`powersync` - `v1.11.1`](#powersync---v1111) + - [`powersync_sqlcipher` - `v0.1.3`](#powersync_sqlcipher---v013) + +--- + +#### `powersync` - `v1.11.1` + + - Fix `statusStream` emitting the same sync status multiple times. + +#### `powersync_sqlcipher` - `v0.1.3` + + - Fix `statusStream` emitting the same sync status multiple times. + + +## 2025-01-06 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`powersync_sqlcipher` - `v0.1.2`](#powersync_sqlcipher---v012) + - [`powersync` - `v1.11.0`](#powersync---v1110) + - [`powersync_attachments_helper` - `v0.6.17`](#powersync_attachments_helper---v0617) + - [`powersync_core` - `v1.1.0`](#powersync_core---v110) + - [`powersync_flutter_libs` - `v0.4.4`](#powersync_flutter_libs---v044) + +--- + +#### `powersync_sqlcipher` - `v0.1.2` + +#### `powersync` - `v1.11.0` + + - Increase limit on number of columns per table to 1999. + - Avoid deleting the $local bucket on connect(). + +#### `powersync_attachments_helper` - `v0.6.17` + + - Update dependencies. + +#### `powersync_core` - `v1.1.0` + + - Increase limit on number of columns per table to 1999. + - Avoid deleting the $local bucket on connect(). + +#### `powersync_flutter_libs` - `v0.4.4` + + - powersync-sqlite-core 0.3.8 - increases column limit and fixes view migration issue + + +## 2024-11-13 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`powersync_sqlcipher` - `v0.1.1`](#powersync_sqlcipher---v011) + +--- + +#### `powersync_sqlcipher` - `v0.1.1` + + - Update dependency `powersync_flutter_libs` to v0.4.3 + + +## 2024-11-12 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`powersync` - `v1.10.0`](#powersync---v1100) + - [`powersync_attachments_helper` - `v0.6.16`](#powersync_attachments_helper---v0616) + - [`powersync_core` - `v1.0.0`](#powersync_core---v100) + - [`powersync_sqlcipher` - `v0.1.0`](#powersync_sqlcipher---v010) + +--- + +#### `powersync` - `v1.10.0` + + - This package now uses the `powersync_core` package to provide its base functionality. + +#### `powersync_attachments_helper` - `v0.6.16` + + - Update a dependency to the latest release. + +#### `powersync_core` - `v1.0.0` + + - Dart library for Powersync for use cases such as server-side Dart or non-Flutter Dart environments initial release. + +#### `powersync_sqlcipher` - `v0.1.0` + + - PowerSync client SDK for Flutter with encryption enabled using SQLCipher initial release. + + ## 2024-11-11 ### Changes diff --git a/README.md b/README.md index db1aa8a7..0f203caf 100644 --- a/README.md +++ b/README.md @@ -2,21 +2,23 @@

-*[PowerSync](https://www.powersync.com) is a sync engine for building local-first apps with instantly-responsive UI/UX and simplified state transfer. Syncs between SQLite on the client-side and Postgres, MongoDB or MySQL on the server-side.* +_[PowerSync](https://www.powersync.com) is a sync engine for building local-first apps with instantly-responsive UI/UX and simplified state transfer. Syncs between SQLite on the client-side and Postgres, MongoDB or MySQL on the server-side._ -PowerSync SDK for Dart and Flutter -=========== +# PowerSync SDK for Dart and Flutter -| package | build | pub | likes | popularity | pub points | -|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------| ------- | ------- | -| [powersync](https://github.com/powersync-ja/powersync.dart/tree/master/packages/powersync) | [![build](https://github.com/powersync-ja/powersync.dart/actions/workflows/packages.yml/badge.svg?branch=master)](https://github.com/powersync-ja/powersync.dart/actions?query=workflow%3Apackages) | [![pub package](https://img.shields.io/pub/v/powersync.svg)](https://pub.dev/packages/powersync) | [![likes](https://img.shields.io/pub/likes/powersync?logo=dart)](https://pub.dev/packages/powersync/score) | [![popularity](https://img.shields.io/pub/popularity/powersync?logo=dart)](https://pub.dev/packages/powersync/score) | [![pub points](https://img.shields.io/pub/points/powersync?logo=dart)](https://pub.dev/packages/powersync/score) -| [powersync_attachments_helper](https://github.com/powersync-ja/powersync.dart/tree/master/packages/powersync_attachments_helper) | [![build](https://github.com/powersync-ja/powersync.dart/actions/workflows/packages.yml/badge.svg?branch=master)](https://github.com/powersync-ja/powersync.dart/actions?query=workflow%3Apackages) | [![pub package](https://img.shields.io/pub/v/powersync_attachments_helper.svg)](https://pub.dev/packages/powersync_attachments_helper) | [![likes](https://img.shields.io/pub/likes/powersync_attachments_helper?logo=dart)](https://pub.dev/packages/powersync_attachments_helper/score) | [![popularity](https://img.shields.io/pub/popularity/powersync_attachments_helper?logo=dart)](https://pub.dev/packages/powersync_attachments_helper/score) | [![pub points](https://img.shields.io/pub/points/powersync_attachments_helper?logo=dart)](https://pub.dev/packages/powersync_attachments_helper/score) +| package | build | pub | likes | pub points | +|--------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| +| [powersync](https://github.com/powersync-ja/powersync.dart/tree/main/packages/powersync) | [![build](https://github.com/powersync-ja/powersync.dart/actions/workflows/check.yml/badge.svg?branch=main)](https://github.com/powersync-ja/powersync.dart/actions?query=workflow%3Apackages) | [![pub package](https://img.shields.io/pub/v/powersync.svg)](https://pub.dev/packages/powersync) | [![likes](https://img.shields.io/pub/likes/powersync?logo=dart)](https://pub.dev/packages/powersync/score) | [![pub points](https://img.shields.io/pub/points/powersync?logo=dart)](https://pub.dev/packages/powersync/score) | +| [powersync_core](https://github.com/powersync-ja/powersync.dart/tree/main/packages/powersync_core) | [![build](https://github.com/powersync-ja/powersync.dart/actions/workflows/check.yml/badge.svg?branch=main)](https://github.com/powersync-ja/powersync.dart/actions?query=workflow%3Apackages) | [![pub package](https://img.shields.io/pub/v/powersync_core.svg)](https://pub.dev/packages/powersync_core) | [![likes](https://img.shields.io/pub/likes/powersync_core?logo=dart)](https://pub.dev/packages/powersync_core/score) | [![pub points](https://img.shields.io/pub/points/powersync_core?logo=dart)](https://pub.dev/packages/powersync_core/score) | +| [powersync_sqlcipher](https://github.com/powersync-ja/powersync.dart/tree/main/packages/powersync_sqlcipher) | [![build](https://github.com/powersync-ja/powersync.dart/actions/workflows/check.yml/badge.svg?branch=main)](https://github.com/powersync-ja/powersync.dart/actions?query=workflow%3Apackages) | [![pub package](https://img.shields.io/pub/v/powersync_sqlcipher.svg)](https://pub.dev/packages/powersync_sqlcipher) | [![likes](https://img.shields.io/pub/likes/powersync_sqlcipher?logo=dart)](https://pub.dev/packages/powersync_sqlcipher/score) | [![pub points](https://img.shields.io/pub/points/powersync_sqlcipher?logo=dart)](https://pub.dev/packages/powersync_sqlcipher/score) | +| [powersync_attachments_helper](https://github.com/powersync-ja/powersync.dart/tree/main/packages/powersync_attachments_helper) | [![build](https://github.com/powersync-ja/powersync.dart/actions/workflows/check.yml/badge.svg?branch=main)](https://github.com/powersync-ja/powersync.dart/actions?query=workflow%3Apackages) | [![pub package](https://img.shields.io/pub/v/powersync_attachments_helper.svg)](https://pub.dev/packages/powersync_attachments_helper) | [![likes](https://img.shields.io/pub/likes/powersync_attachments_helper?logo=dart)](https://pub.dev/packages/powersync_attachments_helper/score) | [![pub points](https://img.shields.io/pub/points/powersync_attachments_helper?logo=dart)](https://pub.dev/packages/powersync_attachments_helper/score) | +| [powersync_flutter_libs](https://github.com/powersync-ja/powersync.dart/tree/main/packages/powersync_flutter_libs) | [![build](https://github.com/powersync-ja/powersync.dart/actions/workflows/check.yml/badge.svg?branch=main)](https://github.com/powersync-ja/powersync.dart/actions?query=workflow%3Apackages) | [![pub package](https://img.shields.io/pub/v/powersync_flutter_libs.svg)](https://pub.dev/packages/powersync_flutter_libs) | [![likes](https://img.shields.io/pub/likes/powersync_flutter_libs?logo=dart)](https://pub.dev/packages/powersync_flutter_libs/score) | [![pub points](https://img.shields.io/pub/points/powersync_flutter_libs?logo=dart)](https://pub.dev/packages/powersync_flutter_libs/score) | #### Usage This monorepo uses [melos](https://melos.invertase.dev/) to handle command and package management. -For detailed usage, check out the inner [powersync](https://github.com/powersync-ja/powersync.dart/tree/master/packages/powersync) and [attachments helper](https://github.com/powersync-ja/powersync.dart/tree/master/packages/powersync_attachments_helper) packages. +For detailed usage, check out the inner [powersync](https://github.com/powersync-ja/powersync.dart/tree/main/packages/powersync), [powersync_core](https://github.com/powersync-ja/powersync.dart/tree/main/packages/powersync_core), [powersync_sqlcipher](https://github.com/powersync-ja/powersync.dart/tree/main/packages/powersync_sqlcipher) and [attachments helper](https://github.com/powersync-ja/powersync.dart/tree/main/packages/powersync_attachments_helper) packages. To configure the monorepo for development run `melos prepare` after cloning diff --git a/demos/benchmarks/.gitignore b/demos/benchmarks/.gitignore index 1a825b5b..0f3655d3 100644 --- a/demos/benchmarks/.gitignore +++ b/demos/benchmarks/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related diff --git a/demos/benchmarks/ios/Flutter/Debug.xcconfig b/demos/benchmarks/ios/Flutter/Debug.xcconfig index 592ceee8..ec97fc6f 100644 --- a/demos/benchmarks/ios/Flutter/Debug.xcconfig +++ b/demos/benchmarks/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/demos/benchmarks/ios/Flutter/Release.xcconfig b/demos/benchmarks/ios/Flutter/Release.xcconfig index 592ceee8..c4855bfe 100644 --- a/demos/benchmarks/ios/Flutter/Release.xcconfig +++ b/demos/benchmarks/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/demos/supabase-todolist-drift/ios/Podfile b/demos/benchmarks/ios/Podfile similarity index 98% rename from demos/supabase-todolist-drift/ios/Podfile rename to demos/benchmarks/ios/Podfile index d97f17e2..3e44f9c6 100644 --- a/demos/supabase-todolist-drift/ios/Podfile +++ b/demos/benchmarks/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/demos/benchmarks/ios/Podfile.lock b/demos/benchmarks/ios/Podfile.lock new file mode 100644 index 00000000..9d0fed8c --- /dev/null +++ b/demos/benchmarks/ios/Podfile.lock @@ -0,0 +1,64 @@ +PODS: + - Flutter (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - powersync-sqlite-core (0.4.5) + - powersync_flutter_libs (0.0.1): + - Flutter + - powersync-sqlite-core (~> 0.4.5) + - sqlite3 (3.49.2): + - sqlite3/common (= 3.49.2) + - sqlite3/common (3.49.2) + - sqlite3/dbstatvtab (3.49.2): + - sqlite3/common + - sqlite3/fts5 (3.49.2): + - sqlite3/common + - sqlite3/math (3.49.2): + - sqlite3/common + - sqlite3/perf-threadsafe (3.49.2): + - sqlite3/common + - sqlite3/rtree (3.49.2): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (~> 3.49.1) + - sqlite3/dbstatvtab + - sqlite3/fts5 + - sqlite3/math + - sqlite3/perf-threadsafe + - sqlite3/rtree + +DEPENDENCIES: + - Flutter (from `Flutter`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - powersync_flutter_libs (from `.symlinks/plugins/powersync_flutter_libs/ios`) + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) + +SPEC REPOS: + trunk: + - powersync-sqlite-core + - sqlite3 + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + powersync_flutter_libs: + :path: ".symlinks/plugins/powersync_flutter_libs/ios" + sqlite3_flutter_libs: + :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" + +SPEC CHECKSUMS: + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + powersync-sqlite-core: 6f32860379009d2a37cadc9e9427a431bdbd83c8 + powersync_flutter_libs: 7684a62208907328906eb932f1fc8b3d8879974e + sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 + sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 + +PODFILE CHECKSUM: a57f30d18f102dd3ce366b1d62a55ecbef2158e5 + +COCOAPODS: 1.16.2 diff --git a/demos/benchmarks/ios/Runner.xcodeproj/project.pbxproj b/demos/benchmarks/ios/Runner.xcodeproj/project.pbxproj index cc46d302..cfbe4698 100644 --- a/demos/benchmarks/ios/Runner.xcodeproj/project.pbxproj +++ b/demos/benchmarks/ios/Runner.xcodeproj/project.pbxproj @@ -10,6 +10,8 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 55BE373CCF1ED6423F7C9F13 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D17B4AC493F667F33A6813D1 /* Pods_RunnerTests.framework */; }; + 6A8AE5F30479FC94C3BB9A5F /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B97B2AC19F853FF3853B5B33 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -42,9 +44,11 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1908EB9C1E3CC625229434C8 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 720A51B109D82B5D4BF97A08 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -55,19 +59,43 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AF9A1EC87465D4CBAC91D6D6 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + B97B2AC19F853FF3853B5B33 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C574CE2DA7DA60333EA773D8 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + D17B4AC493F667F33A6813D1 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D9BAE173C885D5787C363140 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + EFDD574E630EAAEB09F77C18 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 5D7C45551C3DD3F3DB7C2457 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 55BE373CCF1ED6423F7C9F13 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 6A8AE5F30479FC94C3BB9A5F /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 27D7B259814214C290142B81 /* Frameworks */ = { + isa = PBXGroup; + children = ( + B97B2AC19F853FF3853B5B33 /* Pods_Runner.framework */, + D17B4AC493F667F33A6813D1 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 331C8082294A63A400263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( @@ -94,6 +122,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + A18EAFE14B4E966837146A32 /* Pods */, + 27D7B259814214C290142B81 /* Frameworks */, ); sourceTree = ""; }; @@ -121,6 +151,20 @@ path = Runner; sourceTree = ""; }; + A18EAFE14B4E966837146A32 /* Pods */ = { + isa = PBXGroup; + children = ( + 720A51B109D82B5D4BF97A08 /* Pods-Runner.debug.xcconfig */, + AF9A1EC87465D4CBAC91D6D6 /* Pods-Runner.release.xcconfig */, + 1908EB9C1E3CC625229434C8 /* Pods-Runner.profile.xcconfig */, + EFDD574E630EAAEB09F77C18 /* Pods-RunnerTests.debug.xcconfig */, + D9BAE173C885D5787C363140 /* Pods-RunnerTests.release.xcconfig */, + C574CE2DA7DA60333EA773D8 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -128,8 +172,10 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 61C5943B6BDAA164BA64B268 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, + 5D7C45551C3DD3F3DB7C2457 /* Frameworks */, ); buildRules = ( ); @@ -145,12 +191,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 2D6D7232986C3A8199CBAED1 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + EFB7574570F19652665668DF /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -222,6 +270,28 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 2D6D7232986C3A8199CBAED1 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -238,6 +308,28 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 61C5943B6BDAA164BA64B268 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -253,6 +345,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + EFB7574570F19652665668DF /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -378,6 +487,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = EFDD574E630EAAEB09F77C18 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -395,6 +505,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = D9BAE173C885D5787C363140 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -410,6 +521,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = C574CE2DA7DA60333EA773D8 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; diff --git a/demos/benchmarks/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demos/benchmarks/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 8e3ca5df..15cada48 100644 --- a/demos/benchmarks/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/demos/benchmarks/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -59,6 +59,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/demos/benchmarks/ios/Runner.xcworkspace/contents.xcworkspacedata b/demos/benchmarks/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16..21a3cc14 100644 --- a/demos/benchmarks/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/demos/benchmarks/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/demos/benchmarks/ios/Runner/AppDelegate.swift b/demos/benchmarks/ios/Runner/AppDelegate.swift index 9074fee9..62666446 100644 --- a/demos/benchmarks/ios/Runner/AppDelegate.swift +++ b/demos/benchmarks/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import Flutter import UIKit -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/demos/benchmarks/lib/powersync.dart b/demos/benchmarks/lib/powersync.dart index 8d5baa4d..df192ee0 100644 --- a/demos/benchmarks/lib/powersync.dart +++ b/demos/benchmarks/lib/powersync.dart @@ -88,13 +88,15 @@ Future getDatabasePath() async { var currentConnector = BackendConnector(); +const options = SyncOptions( + params: {'size_bucket': AppConfig.sizeBucket}, + crudThrottleTime: Duration(milliseconds: 1), +); + Future resync() async { await db.disconnectAndClear(); timer.start(db); - db.connect( - connector: currentConnector, - params: {'size_bucket': AppConfig.sizeBucket}, - crudThrottleTime: const Duration(milliseconds: 1)); + db.connect(connector: currentConnector, options: options); } Future openDatabase() async { @@ -106,8 +108,5 @@ Future openDatabase() async { BenchmarkItem.updateItemBenchmarks(); timer.start(db); - db.connect( - connector: currentConnector, - params: {'size_bucket': AppConfig.sizeBucket}, - crudThrottleTime: const Duration(milliseconds: 1)); + db.connect(connector: currentConnector, options: options); } diff --git a/demos/benchmarks/macos/Flutter/Flutter-Debug.xcconfig b/demos/benchmarks/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b6..4b81f9b2 100644 --- a/demos/benchmarks/macos/Flutter/Flutter-Debug.xcconfig +++ b/demos/benchmarks/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/demos/benchmarks/macos/Flutter/Flutter-Release.xcconfig b/demos/benchmarks/macos/Flutter/Flutter-Release.xcconfig index c2efd0b6..5caa9d15 100644 --- a/demos/benchmarks/macos/Flutter/Flutter-Release.xcconfig +++ b/demos/benchmarks/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/demos/supabase-todolist-drift/macos/Podfile b/demos/benchmarks/macos/Podfile similarity index 98% rename from demos/supabase-todolist-drift/macos/Podfile rename to demos/benchmarks/macos/Podfile index c795730d..b52666a1 100644 --- a/demos/supabase-todolist-drift/macos/Podfile +++ b/demos/benchmarks/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.14' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/demos/benchmarks/macos/Podfile.lock b/demos/benchmarks/macos/Podfile.lock new file mode 100644 index 00000000..76db1f9b --- /dev/null +++ b/demos/benchmarks/macos/Podfile.lock @@ -0,0 +1,64 @@ +PODS: + - FlutterMacOS (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - powersync-sqlite-core (0.4.5) + - powersync_flutter_libs (0.0.1): + - FlutterMacOS + - powersync-sqlite-core (~> 0.4.5) + - sqlite3 (3.49.2): + - sqlite3/common (= 3.49.2) + - sqlite3/common (3.49.2) + - sqlite3/dbstatvtab (3.49.2): + - sqlite3/common + - sqlite3/fts5 (3.49.2): + - sqlite3/common + - sqlite3/math (3.49.2): + - sqlite3/common + - sqlite3/perf-threadsafe (3.49.2): + - sqlite3/common + - sqlite3/rtree (3.49.2): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (~> 3.49.1) + - sqlite3/dbstatvtab + - sqlite3/fts5 + - sqlite3/math + - sqlite3/perf-threadsafe + - sqlite3/rtree + +DEPENDENCIES: + - FlutterMacOS (from `Flutter/ephemeral`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - powersync_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/powersync_flutter_libs/macos`) + - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`) + +SPEC REPOS: + trunk: + - powersync-sqlite-core + - sqlite3 + +EXTERNAL SOURCES: + FlutterMacOS: + :path: Flutter/ephemeral + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + powersync_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/powersync_flutter_libs/macos + sqlite3_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin + +SPEC CHECKSUMS: + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + powersync-sqlite-core: 6f32860379009d2a37cadc9e9427a431bdbd83c8 + powersync_flutter_libs: 41d8a7b193abf15e46f95f0ec1229d86b6893171 + sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 + sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 + +PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 + +COCOAPODS: 1.16.2 diff --git a/demos/benchmarks/macos/Runner.xcodeproj/project.pbxproj b/demos/benchmarks/macos/Runner.xcodeproj/project.pbxproj index ffb0390c..960a5a58 100644 --- a/demos/benchmarks/macos/Runner.xcodeproj/project.pbxproj +++ b/demos/benchmarks/macos/Runner.xcodeproj/project.pbxproj @@ -27,6 +27,8 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 9093F47C0B6EAD77C2B783DA /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8FCB7D15769DA95071391D1B /* Pods_Runner.framework */; }; + A10E6A04FDE0D447F4342C4E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 65910ED980588B63D8A150F4 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -60,11 +62,12 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 1464422B31F3DF1298E426C6 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* benchmarks.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "benchmarks.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* benchmarks.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = benchmarks.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -76,8 +79,15 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 65910ED980588B63D8A150F4 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 87BDA74CA76A55849C7F34EF /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 8FCB7D15769DA95071391D1B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 95ED6FD4C60383CCCA983998 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + BC600AD1B46FC8B47BF4A584 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + DED1D204ED98DCE172D8FD06 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + F90CD46D97CB5DFA79A60335 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,6 +95,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A10E6A04FDE0D447F4342C4E /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -92,6 +103,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 9093F47C0B6EAD77C2B783DA /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -125,6 +137,7 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + 83FD9C0045A6BE42E567DFFF /* Pods */, ); sourceTree = ""; }; @@ -172,9 +185,25 @@ path = Runner; sourceTree = ""; }; + 83FD9C0045A6BE42E567DFFF /* Pods */ = { + isa = PBXGroup; + children = ( + BC600AD1B46FC8B47BF4A584 /* Pods-Runner.debug.xcconfig */, + 95ED6FD4C60383CCCA983998 /* Pods-Runner.release.xcconfig */, + F90CD46D97CB5DFA79A60335 /* Pods-Runner.profile.xcconfig */, + 1464422B31F3DF1298E426C6 /* Pods-RunnerTests.debug.xcconfig */, + 87BDA74CA76A55849C7F34EF /* Pods-RunnerTests.release.xcconfig */, + DED1D204ED98DCE172D8FD06 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + 8FCB7D15769DA95071391D1B /* Pods_Runner.framework */, + 65910ED980588B63D8A150F4 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -186,6 +215,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + C4E73F96DDA098F470016E9F /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -204,11 +234,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 294F0AD87F4F87D46586033D /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + 2552B7F32AADACDD56A1879C /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -291,6 +323,45 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 2552B7F32AADACDD56A1879C /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 294F0AD87F4F87D46586033D /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -329,6 +400,28 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + C4E73F96DDA098F470016E9F /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -380,6 +473,7 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 1464422B31F3DF1298E426C6 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -394,6 +488,7 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 87BDA74CA76A55849C7F34EF /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -408,6 +503,7 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = DED1D204ED98DCE172D8FD06 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; diff --git a/demos/benchmarks/macos/Runner.xcworkspace/contents.xcworkspacedata b/demos/benchmarks/macos/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16..21a3cc14 100644 --- a/demos/benchmarks/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/demos/benchmarks/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/demos/benchmarks/pubspec.lock b/demos/benchmarks/pubspec.lock index 687f1f3c..b45942df 100644 --- a/demos/benchmarks/pubspec.lock +++ b/demos/benchmarks/pubspec.lock @@ -5,34 +5,34 @@ packages: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.7.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -45,66 +45,50 @@ packages: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" crypto: dependency: transitive description: name: crypto - sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.6" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" - fetch_api: - dependency: transitive - description: - name: fetch_api - sha256: "97f46c25b480aad74f7cc2ad7ccba2c5c6f08d008e68f95c1077286ce243d0e6" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - fetch_client: - dependency: transitive - description: - name: fetch_client - sha256: "9666ee14536778474072245ed5cba07db81ae8eb5de3b7bf4a2d1e2c49696092" - url: "https://pub.dev" - source: hosted - version: "1.1.2" + version: "1.3.3" ffi: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -127,26 +111,18 @@ packages: dependency: "direct main" description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.5.0" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" - js: - dependency: transitive - description: - name: js - sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf - url: "https://pub.dev" - source: hosted - version: "0.7.1" + version: "4.1.2" json_annotation: dependency: transitive description: @@ -159,26 +135,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "11.0.1" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -191,18 +167,18 @@ packages: dependency: "direct main" description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -215,10 +191,10 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mutex: dependency: transitive description: @@ -231,34 +207,34 @@ packages: dependency: "direct main" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_provider: dependency: "direct main" description: name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.10" + version: "2.2.17" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -287,10 +263,10 @@ packages: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -305,43 +281,50 @@ packages: path: "../../packages/powersync" relative: true source: path - version: "1.9.2" + version: "1.15.2" + powersync_core: + dependency: "direct overridden" + description: + path: "../../packages/powersync_core" + relative: true + source: path + version: "1.5.2" powersync_flutter_libs: dependency: "direct overridden" description: path: "../../packages/powersync_flutter_libs" relative: true source: path - version: "0.4.2" + version: "0.4.11" pub_semver: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" sprintf: dependency: transitive description: @@ -354,82 +337,82 @@ packages: dependency: transitive description: name: sqlite3 - sha256: bb174b3ec2527f9c5f680f73a89af8149dd99782fbb56ea88ad0807c5638f2ed + sha256: f393d92c71bdcc118d6203d07c991b9be0f84b1a6f89dd4f7eed348131329924 url: "https://pub.dev" source: hosted - version: "2.4.7" + version: "2.9.0" sqlite3_flutter_libs: dependency: transitive description: name: sqlite3_flutter_libs - sha256: "62bbb4073edbcdf53f40c80775f33eea01d301b7b81417e5b3fb7395416258c1" + sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14" url: "https://pub.dev" source: hosted - version: "0.5.24" + version: "0.5.32" sqlite3_web: dependency: transitive description: name: sqlite3_web - sha256: f22d1dda7a40be0867984f55cdf5c2d599e5f05d3be4a642d78f38b38983f554 + sha256: "0f6ebcb4992d1892ac5c8b5ecd22a458ab9c5eb6428b11ae5ecb5d63545844da" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.3.2" sqlite_async: dependency: "direct main" description: name: sqlite_async - sha256: d66fb6e6d07c1a834743326c033029f75becbb1fad6823d709f921872abc3d5b + sha256: "6116bfc6aef6ce77730b478385ba4a58873df45721f6a9bc6ffabf39b6576e36" url: "https://pub.dev" source: hosted - version: "0.11.0" + version: "0.12.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.6" typed_data: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" universal_io: dependency: "direct main" description: @@ -442,50 +425,50 @@ packages: dependency: transitive description: name: uuid - sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "4.4.2" + version: "4.5.1" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "15.0.0" web: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" yaml: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + dart: ">=3.8.0-0 <4.0.0" + flutter: ">=3.27.0" diff --git a/demos/benchmarks/pubspec.yaml b/demos/benchmarks/pubspec.yaml index 8e360c04..aea63c20 100644 --- a/demos/benchmarks/pubspec.yaml +++ b/demos/benchmarks/pubspec.yaml @@ -10,12 +10,12 @@ environment: dependencies: flutter: sdk: flutter - powersync: ^1.9.3 + powersync: ^1.16.1 path_provider: ^2.1.1 path: ^1.8.3 logging: ^1.2.0 universal_io: ^2.2.2 - sqlite_async: ^0.11.0 + sqlite_async: ^0.12.0 http: ^1.2.2 dev_dependencies: diff --git a/demos/django-todolist/.gitignore b/demos/django-todolist/.gitignore index 0b04140a..777f9a68 100644 --- a/demos/django-todolist/.gitignore +++ b/demos/django-todolist/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related diff --git a/demos/django-todolist/ios/Podfile b/demos/django-todolist/ios/Podfile index e9f73048..2c1e086a 100644 --- a/demos/django-todolist/ios/Podfile +++ b/demos/django-todolist/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '12.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/demos/django-todolist/ios/Podfile.lock b/demos/django-todolist/ios/Podfile.lock index c47483fd..8bfa9ae3 100644 --- a/demos/django-todolist/ios/Podfile.lock +++ b/demos/django-todolist/ios/Podfile.lock @@ -3,29 +3,33 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - powersync-sqlite-core (0.3.0) + - powersync-sqlite-core (0.4.5) - powersync_flutter_libs (0.0.1): - Flutter - - powersync-sqlite-core (~> 0.3.0) + - powersync-sqlite-core (~> 0.4.5) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - "sqlite3 (3.46.1+1)": - - "sqlite3/common (= 3.46.1+1)" - - "sqlite3/common (3.46.1+1)" - - "sqlite3/dbstatvtab (3.46.1+1)": + - sqlite3 (3.49.2): + - sqlite3/common (= 3.49.2) + - sqlite3/common (3.49.2) + - sqlite3/dbstatvtab (3.49.2): - sqlite3/common - - "sqlite3/fts5 (3.46.1+1)": + - sqlite3/fts5 (3.49.2): - sqlite3/common - - "sqlite3/perf-threadsafe (3.46.1+1)": + - sqlite3/math (3.49.2): - sqlite3/common - - "sqlite3/rtree (3.46.1+1)": + - sqlite3/perf-threadsafe (3.49.2): + - sqlite3/common + - sqlite3/rtree (3.49.2): - sqlite3/common - sqlite3_flutter_libs (0.0.1): - Flutter - - "sqlite3 (~> 3.46.0+1)" + - FlutterMacOS + - sqlite3 (~> 3.49.1) - sqlite3/dbstatvtab - sqlite3/fts5 + - sqlite3/math - sqlite3/perf-threadsafe - sqlite3/rtree @@ -34,7 +38,7 @@ DEPENDENCIES: - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - powersync_flutter_libs (from `.symlinks/plugins/powersync_flutter_libs/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) SPEC REPOS: trunk: @@ -51,17 +55,17 @@ EXTERNAL SOURCES: shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqlite3_flutter_libs: - :path: ".symlinks/plugins/sqlite3_flutter_libs/ios" + :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" SPEC CHECKSUMS: - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - powersync-sqlite-core: ad0e70e23bacd858fe2e79032dc4aabdf972d1bd - powersync_flutter_libs: 064c44b51fb07df9486b735fb96ab7608a89e18b - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb - sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + powersync-sqlite-core: 6f32860379009d2a37cadc9e9427a431bdbd83c8 + powersync_flutter_libs: 7684a62208907328906eb932f1fc8b3d8879974e + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 + sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 -PODFILE CHECKSUM: f7b3cb7384a2d5da4b22b090e1f632de7f377987 +PODFILE CHECKSUM: 2c1730c97ea13f1ea48b32e9c79de785b4f2f02f -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/demos/django-todolist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demos/django-todolist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5e31d3d3..c53e2b31 100644 --- a/demos/django-todolist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/demos/django-todolist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -48,6 +48,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/demos/django-todolist/ios/Runner/AppDelegate.swift b/demos/django-todolist/ios/Runner/AppDelegate.swift index 70693e4a..b6363034 100644 --- a/demos/django-todolist/ios/Runner/AppDelegate.swift +++ b/demos/django-todolist/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/demos/django-todolist/lib/main.dart b/demos/django-todolist/lib/main.dart index 5fad8f77..f053b806 100644 --- a/demos/django-todolist/lib/main.dart +++ b/demos/django-todolist/lib/main.dart @@ -41,7 +41,7 @@ const listsPage = ListsPage(); const homePage = listsPage; const sqlConsolePage = Scaffold( - appBar: StatusAppBar(title: 'SQL Console'), + appBar: StatusAppBar(title: Text('SQL Console')), body: QueryWidget(defaultQuery: defaultQuery)); const loginPage = LoginPage(); @@ -76,7 +76,7 @@ class MyHomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: StatusAppBar(title: title), + appBar: StatusAppBar(title: Text(title)), body: Center(child: content), floatingActionButton: floatingActionButton, drawer: Drawer( diff --git a/demos/django-todolist/lib/widgets/guard_by_sync.dart b/demos/django-todolist/lib/widgets/guard_by_sync.dart new file mode 100644 index 00000000..b65986b0 --- /dev/null +++ b/demos/django-todolist/lib/widgets/guard_by_sync.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:powersync/powersync.dart' hide Column; +import 'package:powersync_django_todolist_demo/powersync.dart'; + +/// A widget that shows [child] after a complete sync on the database has +/// completed and a progress bar before that. +class GuardBySync extends StatelessWidget { + final Widget child; + + /// When set, wait only for a complete sync within the [StreamPriority] + /// instead of a full sync. + final StreamPriority? priority; + + const GuardBySync({ + super.key, + required this.child, + this.priority, + }); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: db.statusStream, + initialData: db.currentStatus, + builder: (context, snapshot) { + final status = snapshot.requireData; + final (didSync, progress) = switch (priority) { + null => (status.hasSynced ?? false, status.downloadProgress), + var priority? => ( + status.statusForPriority(priority).hasSynced ?? false, + status.downloadProgress?.untilPriority(priority) + ), + }; + + if (didSync) { + return child; + } else { + return Center( + child: Column( + children: [ + const Text('Busy with sync...'), + LinearProgressIndicator(value: progress?.downloadedFraction), + if (progress case final progress?) + Text( + '${progress.downloadedOperations} out of ${progress.totalOperations}') + ], + ), + ); + } + }, + ); + } +} diff --git a/demos/django-todolist/lib/widgets/lists_page.dart b/demos/django-todolist/lib/widgets/lists_page.dart index e31c2fc8..f60f158b 100644 --- a/demos/django-todolist/lib/widgets/lists_page.dart +++ b/demos/django-todolist/lib/widgets/lists_page.dart @@ -1,11 +1,10 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import './list_item.dart'; import './list_item_dialog.dart'; import '../main.dart'; import '../models/todo_list.dart'; +import 'guard_by_sync.dart'; void _showAddDialog(BuildContext context) async { return showDialog( @@ -41,48 +40,27 @@ class ListsPage extends StatelessWidget { } } -class ListsWidget extends StatefulWidget { +final class ListsWidget extends StatelessWidget { const ListsWidget({super.key}); - @override - State createState() { - return _ListsWidgetState(); - } -} - -class _ListsWidgetState extends State { - List _data = []; - StreamSubscription? _subscription; - - _ListsWidgetState(); - - @override - void initState() { - super.initState(); - final stream = TodoList.watchListsWithStats(); - _subscription = stream.listen((data) { - if (!context.mounted) { - return; - } - setState(() { - _data = data; - }); - }); - } - - @override - void dispose() { - super.dispose(); - _subscription?.cancel(); - } - @override Widget build(BuildContext context) { - return ListView( - padding: const EdgeInsets.symmetric(vertical: 8.0), - children: _data.map((list) { - return ListItemWidget(list: list); - }).toList(), + return GuardBySync( + child: StreamBuilder( + stream: TodoList.watchListsWithStats(), + builder: (context, snapshot) { + if (snapshot.data case final todoLists?) { + return ListView( + padding: const EdgeInsets.symmetric(vertical: 8.0), + children: todoLists.map((list) { + return ListItemWidget(list: list); + }).toList(), + ); + } else { + return const CircularProgressIndicator(); + } + }, + ), ); } } diff --git a/demos/django-todolist/lib/widgets/status_app_bar.dart b/demos/django-todolist/lib/widgets/status_app_bar.dart index 90d18ae8..9011303e 100644 --- a/demos/django-todolist/lib/widgets/status_app_bar.dart +++ b/demos/django-todolist/lib/widgets/status_app_bar.dart @@ -1,62 +1,42 @@ -import 'dart:async'; - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:powersync/powersync.dart'; import 'package:powersync_django_todolist_demo/widgets/fts_search_delegate.dart'; import '../powersync.dart'; -class StatusAppBar extends StatefulWidget implements PreferredSizeWidget { - const StatusAppBar({super.key, required this.title}); - - final String title; +class StatusAppBar extends StatelessWidget implements PreferredSizeWidget { + final Widget title; - @override - State createState() => _StatusAppBarState(); + const StatusAppBar({super.key, required this.title}); @override Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} - -class _StatusAppBarState extends State { - late SyncStatus _connectionState; - StreamSubscription? _syncStatusSubscription; - - @override - void initState() { - super.initState(); - _connectionState = db.currentStatus; - _syncStatusSubscription = db.statusStream.listen((event) { - setState(() { - _connectionState = db.currentStatus; - }); - }); - } - - @override - void dispose() { - super.dispose(); - _syncStatusSubscription?.cancel(); - } @override Widget build(BuildContext context) { - final statusIcon = _getStatusIcon(_connectionState); + return StreamBuilder( + stream: db.statusStream, + initialData: db.currentStatus, + builder: (context, snapshot) { + final status = snapshot.data!; + final statusIcon = _getStatusIcon(status); - return AppBar( - title: Text(widget.title), - actions: [ - IconButton( - onPressed: () { - showSearch(context: context, delegate: FtsSearchDelegate()); - }, - icon: const Icon(Icons.search), - ), - statusIcon, - // Make some space for the "Debug" banner, so that the status - // icon isn't hidden - if (kDebugMode) _makeIcon('Debug mode', Icons.developer_mode), - ], + return AppBar( + title: title, + actions: [ + IconButton( + onPressed: () { + showSearch(context: context, delegate: FtsSearchDelegate()); + }, + icon: const Icon(Icons.search), + ), + statusIcon, + // Make some space for the "Debug" banner, so that the status + // icon isn't hidden + if (kDebugMode) _makeIcon('Debug mode', Icons.developer_mode), + ], + ); + }, ); } } diff --git a/demos/django-todolist/lib/widgets/todo_list_page.dart b/demos/django-todolist/lib/widgets/todo_list_page.dart index 26dbae63..a457eb98 100644 --- a/demos/django-todolist/lib/widgets/todo_list_page.dart +++ b/demos/django-todolist/lib/widgets/todo_list_page.dart @@ -1,7 +1,4 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; -import 'package:powersync_django_todolist_demo/models/todo_item.dart'; import './status_app_bar.dart'; import './todo_item_dialog.dart'; @@ -34,56 +31,31 @@ class TodoListPage extends StatelessWidget { ); return Scaffold( - appBar: StatusAppBar(title: list.name), + appBar: StatusAppBar(title: Text(list.name)), floatingActionButton: button, body: TodoListWidget(list: list)); } } -class TodoListWidget extends StatefulWidget { +class TodoListWidget extends StatelessWidget { final TodoList list; const TodoListWidget({super.key, required this.list}); - @override - State createState() { - return TodoListWidgetState(); - } -} - -class TodoListWidgetState extends State { - List _data = []; - StreamSubscription? _subscription; - - TodoListWidgetState(); - - @override - void initState() { - super.initState(); - final stream = widget.list.watchItems(); - _subscription = stream.listen((data) { - if (!context.mounted) { - return; - } - setState(() { - _data = data; - }); - }); - } - - @override - void dispose() { - super.dispose(); - _subscription?.cancel(); - } - @override Widget build(BuildContext context) { - return ListView( - padding: const EdgeInsets.symmetric(vertical: 8.0), - children: _data.map((todo) { - return TodoItemWidget(todo: todo); - }).toList(), + return StreamBuilder( + stream: list.watchItems(), + builder: (context, snapshot) { + final items = snapshot.data ?? const []; + + return ListView( + padding: const EdgeInsets.symmetric(vertical: 8.0), + children: items.map((todo) { + return TodoItemWidget(todo: todo); + }).toList(), + ); + }, ); } } diff --git a/demos/django-todolist/macos/Podfile b/demos/django-todolist/macos/Podfile index c795730d..b52666a1 100644 --- a/demos/django-todolist/macos/Podfile +++ b/demos/django-todolist/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.14' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/demos/django-todolist/macos/Podfile.lock b/demos/django-todolist/macos/Podfile.lock index 67953800..c6edd223 100644 --- a/demos/django-todolist/macos/Podfile.lock +++ b/demos/django-todolist/macos/Podfile.lock @@ -3,29 +3,33 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - powersync-sqlite-core (0.1.6) + - powersync-sqlite-core (0.4.5) - powersync_flutter_libs (0.0.1): - FlutterMacOS - - powersync-sqlite-core (~> 0.1.6) + - powersync-sqlite-core (~> 0.4.5) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - "sqlite3 (3.46.0+1)": - - "sqlite3/common (= 3.46.0+1)" - - "sqlite3/common (3.46.0+1)" - - "sqlite3/dbstatvtab (3.46.0+1)": + - sqlite3 (3.49.2): + - sqlite3/common (= 3.49.2) + - sqlite3/common (3.49.2) + - sqlite3/dbstatvtab (3.49.2): - sqlite3/common - - "sqlite3/fts5 (3.46.0+1)": + - sqlite3/fts5 (3.49.2): - sqlite3/common - - "sqlite3/perf-threadsafe (3.46.0+1)": + - sqlite3/math (3.49.2): - sqlite3/common - - "sqlite3/rtree (3.46.0+1)": + - sqlite3/perf-threadsafe (3.49.2): + - sqlite3/common + - sqlite3/rtree (3.49.2): - sqlite3/common - sqlite3_flutter_libs (0.0.1): + - Flutter - FlutterMacOS - - "sqlite3 (~> 3.46.0+1)" + - sqlite3 (~> 3.49.1) - sqlite3/dbstatvtab - sqlite3/fts5 + - sqlite3/math - sqlite3/perf-threadsafe - sqlite3/rtree @@ -34,7 +38,7 @@ DEPENDENCIES: - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - powersync_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/powersync_flutter_libs/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`) + - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`) SPEC REPOS: trunk: @@ -51,17 +55,17 @@ EXTERNAL SOURCES: shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin sqlite3_flutter_libs: - :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos + :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin SPEC CHECKSUMS: - FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - powersync-sqlite-core: 4c38c8f470f6dca61346789fd5436a6826d1e3dd - powersync_flutter_libs: 1eb1c6790a72afe08e68d4cc489d71ab61da32ee - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sqlite3: 292c3e1bfe89f64e51ea7fc7dab9182a017c8630 - sqlite3_flutter_libs: 5ca46c1a04eddfbeeb5b16566164aa7ad1616e7b + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + powersync-sqlite-core: 6f32860379009d2a37cadc9e9427a431bdbd83c8 + powersync_flutter_libs: 41d8a7b193abf15e46f95f0ec1229d86b6893171 + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 + sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 -PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 +PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/demos/django-todolist/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demos/django-todolist/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 2c526589..a0e59441 100644 --- a/demos/django-todolist/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/demos/django-todolist/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -59,6 +59,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/demos/django-todolist/macos/Runner/AppDelegate.swift b/demos/django-todolist/macos/Runner/AppDelegate.swift index d53ef643..b3c17614 100644 --- a/demos/django-todolist/macos/Runner/AppDelegate.swift +++ b/demos/django-todolist/macos/Runner/AppDelegate.swift @@ -1,9 +1,13 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } } diff --git a/demos/django-todolist/macos/Runner/Configs/AppInfo.xcconfig b/demos/django-todolist/macos/Runner/Configs/AppInfo.xcconfig index 797d44b3..d00b6f29 100644 --- a/demos/django-todolist/macos/Runner/Configs/AppInfo.xcconfig +++ b/demos/django-todolist/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = PowerSync Django Demo // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = co.powersync.demotodolist +PRODUCT_BUNDLE_IDENTIFIER = co.powersync.demotodolist.django // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2023 Journey Mobile, Inc. All rights reserved. diff --git a/demos/django-todolist/pubspec.lock b/demos/django-todolist/pubspec.lock index 6f759278..881d6d47 100644 --- a/demos/django-todolist/pubspec.lock +++ b/demos/django-todolist/pubspec.lock @@ -5,34 +5,34 @@ packages: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.7.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -45,74 +45,58 @@ packages: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" crypto: dependency: transitive description: name: crypto - sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.6" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" - fetch_api: - dependency: transitive - description: - name: fetch_api - sha256: "97f46c25b480aad74f7cc2ad7ccba2c5c6f08d008e68f95c1077286ce243d0e6" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - fetch_client: - dependency: transitive - description: - name: fetch_client - sha256: "9666ee14536778474072245ed5cba07db81ae8eb5de3b7bf4a2d1e2c49696092" - url: "https://pub.dev" - source: hosted - version: "1.1.2" + version: "1.3.3" ffi: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -140,26 +124,18 @@ packages: dependency: "direct main" description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.4.0" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" - url: "https://pub.dev" - source: hosted - version: "4.0.2" - js: - dependency: transitive - description: - name: js - sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "4.1.2" json_annotation: dependency: transitive description: @@ -172,18 +148,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -204,18 +180,18 @@ packages: dependency: "direct main" description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -228,10 +204,10 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mutex: dependency: transitive description: @@ -244,34 +220,34 @@ packages: dependency: "direct main" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_provider: dependency: "direct main" description: name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.10" + version: "2.2.17" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -300,10 +276,10 @@ packages: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -318,54 +294,61 @@ packages: path: "../../packages/powersync" relative: true source: path - version: "1.9.2" + version: "1.15.0" + powersync_core: + dependency: "direct overridden" + description: + path: "../../packages/powersync_core" + relative: true + source: path + version: "1.5.0" powersync_flutter_libs: dependency: "direct overridden" description: path: "../../packages/powersync_flutter_libs" relative: true source: path - version: "0.4.2" + version: "0.4.10" pub_semver: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: a7e8467e9181cef109f601e3f65765685786c1a738a83d7fbbde377589c0d974 + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.10" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.4" shared_preferences_linux: dependency: transitive description: @@ -386,10 +369,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" shared_preferences_windows: dependency: transitive description: @@ -402,15 +385,15 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" sprintf: dependency: transitive description: @@ -423,82 +406,82 @@ packages: dependency: transitive description: name: sqlite3 - sha256: bb174b3ec2527f9c5f680f73a89af8149dd99782fbb56ea88ad0807c5638f2ed + sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e" url: "https://pub.dev" source: hosted - version: "2.4.7" + version: "2.7.5" sqlite3_flutter_libs: dependency: transitive description: name: sqlite3_flutter_libs - sha256: "62bbb4073edbcdf53f40c80775f33eea01d301b7b81417e5b3fb7395416258c1" + sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14" url: "https://pub.dev" source: hosted - version: "0.5.24" + version: "0.5.32" sqlite3_web: dependency: transitive description: name: sqlite3_web - sha256: f22d1dda7a40be0867984f55cdf5c2d599e5f05d3be4a642d78f38b38983f554 + sha256: "967e076442f7e1233bd7241ca61f3efe4c7fc168dac0f38411bdb3bdf471eb3c" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.3.1" sqlite_async: dependency: "direct main" description: name: sqlite_async - sha256: d66fb6e6d07c1a834743326c033029f75becbb1fad6823d709f921872abc3d5b + sha256: a60e8d5c8df8e694933bd5a312c38393e79ad77d784bb91c6f38ba627bfb7aec url: "https://pub.dev" source: hosted - version: "0.11.0" + version: "0.11.4" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.4" typed_data: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" universal_io: dependency: transitive description: @@ -511,10 +494,10 @@ packages: dependency: transitive description: name: uuid - sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "4.4.2" + version: "4.5.1" vector_math: dependency: transitive description: @@ -527,34 +510,34 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "15.0.0" web: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" yaml: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/demos/django-todolist/pubspec.yaml b/demos/django-todolist/pubspec.yaml index 096120d1..4c1cacff 100644 --- a/demos/django-todolist/pubspec.yaml +++ b/demos/django-todolist/pubspec.yaml @@ -10,11 +10,11 @@ environment: dependencies: flutter: sdk: flutter - powersync: ^1.9.3 + powersync: ^1.16.1 path_provider: ^2.1.1 path: ^1.8.3 logging: ^1.2.0 - sqlite_async: ^0.11.0 + sqlite_async: ^0.12.0 http: ^1.2.1 shared_preferences: ^2.2.3 diff --git a/demos/firebase-nodejs-todolist/.gitignore b/demos/firebase-nodejs-todolist/.gitignore new file mode 100644 index 00000000..0b04140a --- /dev/null +++ b/demos/firebase-nodejs-todolist/.gitignore @@ -0,0 +1,50 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# asdf +.tool-versions + +# secrets +app_config.dart diff --git a/demos/firebase-nodejs-todolist/LICENSE b/demos/firebase-nodejs-todolist/LICENSE new file mode 100644 index 00000000..0e259d42 --- /dev/null +++ b/demos/firebase-nodejs-todolist/LICENSE @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/demos/firebase-nodejs-todolist/README.md b/demos/firebase-nodejs-todolist/README.md new file mode 100644 index 00000000..c3c577fd --- /dev/null +++ b/demos/firebase-nodejs-todolist/README.md @@ -0,0 +1,49 @@ +# PowerSync + Node.js + Firebase Auth + Flutter Demo: Todo List App + +Demo app demonstrating use of the PowerSync SDK for Flutter together with a custom Node.js backend and using Firebase for user auth on the client. + +This demo can run alongside the [powersync-nodejs-firebase-backend-todolist-demo](https://github.com/powersync-ja/powersync-nodejs-firebase-backend-todolist-demo) for testing and demo purposes. + +We suggest you first set up the `powersync-nodejs-firebase-backend-todolist-demo` before setting up the client as shown in this repo. + +# Running the app + +Ensure you have [melos](https://melos.invertase.dev/~melos-latest/getting-started) installed. + +1. `cd demos/firebase-nodejs-todolist` +2. `melos prepare` +3. `cp lib/app_config_template.dart lib/app_config.dart` +4. Insert your Supabase and PowerSync project credentials into `lib/app_config.dart` (See instructions below) +5. `flutter run` + +# Add your Firebase app +Follow the step found in [this page](https://firebase.google.com/docs/flutter/setup?platform=ios) from the Firebase docs to login to your Firebase account and to initialize the Firebase credentials. + +# Set up Supabase project + +Create a new Supabase project, and paste and run the contents of [database.sql](./database.sql) in the Supabase SQL editor. + +It does the following: + +1. Create `lists` and `todos` tables. +2. Create a publication called `powersync` for `lists` and `todos`. +3. Enable row level security (RLS), allowing users to only view and edit their own data. +4. Create a trigger to populate some sample data when a user registers. + +We won't be using the Supabase Flutter SDK for this demo, but rather as a hosted PostgresSQL database that the app connects to. + +# Set up PowerSync Instance + +Create a new PowerSync instance, connecting to the database of the Supabase project. + +Then deploy the following sync rules: + +```yaml +bucket_definitions: + user_lists: + # Separate bucket per todo list + parameters: select id as list_id from lists where owner_id = request.user_id() + data: + - select * from lists where id = bucket.list_id + - select * from todos where list_id = bucket.list_id +``` diff --git a/demos/firebase-nodejs-todolist/analysis_options.yaml b/demos/firebase-nodejs-todolist/analysis_options.yaml new file mode 100644 index 00000000..93a247ad --- /dev/null +++ b/demos/firebase-nodejs-todolist/analysis_options.yaml @@ -0,0 +1,32 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options + +analyzer: + exclude: + - lib/firebase.dart # Exclude this as it imports a file that is not checked in diff --git a/demos/firebase-nodejs-todolist/android/.gitignore b/demos/firebase-nodejs-todolist/android/.gitignore new file mode 100644 index 00000000..6f568019 --- /dev/null +++ b/demos/firebase-nodejs-todolist/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/demos/supabase-todolist/android/app/build.gradle b/demos/firebase-nodejs-todolist/android/app/build.gradle similarity index 93% rename from demos/supabase-todolist/android/app/build.gradle rename to demos/firebase-nodejs-todolist/android/app/build.gradle index 9daa778b..ad623aee 100644 --- a/demos/supabase-todolist/android/app/build.gradle +++ b/demos/firebase-nodejs-todolist/android/app/build.gradle @@ -22,6 +22,9 @@ if (flutterVersionName == null) { } apply plugin: 'com.android.application' +// START: FlutterFire Configuration +apply plugin: 'com.google.gms.google-services' +// END: FlutterFire Configuration apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" @@ -46,7 +49,7 @@ android { applicationId "co.powersync.demotodolist" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion 24 + minSdkVersion 23 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/demos/firebase-nodejs-todolist/android/app/google-services.json b/demos/firebase-nodejs-todolist/android/app/google-services.json new file mode 100644 index 00000000..839fb1d1 --- /dev/null +++ b/demos/firebase-nodejs-todolist/android/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "1069616552579", + "project_id": "sample-firebase-ai-app-27d98", + "storage_bucket": "sample-firebase-ai-app-27d98.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:1069616552579:android:d2cb390fea186a49db59b6", + "android_client_info": { + "package_name": "co.powersync.demotodolist" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyDBk2GgaUqLvPWGe6cI0h6G4ZZweS2JKGE" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/demos/firebase-nodejs-todolist/android/app/src/debug/AndroidManifest.xml b/demos/firebase-nodejs-todolist/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..f19dd7d6 --- /dev/null +++ b/demos/firebase-nodejs-todolist/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + diff --git a/demos/firebase-nodejs-todolist/android/app/src/main/AndroidManifest.xml b/demos/firebase-nodejs-todolist/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..55e175c4 --- /dev/null +++ b/demos/firebase-nodejs-todolist/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/demos/supabase-todolist/android/app/src/main/kotlin/co/powersync/demotodolist/MainActivity.kt b/demos/firebase-nodejs-todolist/android/app/src/main/kotlin/co/powersync/demotodolist/MainActivity.kt similarity index 100% rename from demos/supabase-todolist/android/app/src/main/kotlin/co/powersync/demotodolist/MainActivity.kt rename to demos/firebase-nodejs-todolist/android/app/src/main/kotlin/co/powersync/demotodolist/MainActivity.kt diff --git a/demos/firebase-nodejs-todolist/android/app/src/main/res/drawable-v21/launch_background.xml b/demos/firebase-nodejs-todolist/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/demos/firebase-nodejs-todolist/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/demos/firebase-nodejs-todolist/android/app/src/main/res/drawable/launch_background.xml b/demos/firebase-nodejs-todolist/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/demos/firebase-nodejs-todolist/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/demos/firebase-nodejs-todolist/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/firebase-nodejs-todolist/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..db77bb4b Binary files /dev/null and b/demos/firebase-nodejs-todolist/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/demos/firebase-nodejs-todolist/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/firebase-nodejs-todolist/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..17987b79 Binary files /dev/null and b/demos/firebase-nodejs-todolist/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/demos/firebase-nodejs-todolist/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/firebase-nodejs-todolist/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..09d43914 Binary files /dev/null and b/demos/firebase-nodejs-todolist/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/demos/firebase-nodejs-todolist/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/firebase-nodejs-todolist/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..d5f1c8d3 Binary files /dev/null and b/demos/firebase-nodejs-todolist/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/demos/firebase-nodejs-todolist/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/firebase-nodejs-todolist/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..4d6372ee Binary files /dev/null and b/demos/firebase-nodejs-todolist/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/demos/firebase-nodejs-todolist/android/app/src/main/res/values-night/styles.xml b/demos/firebase-nodejs-todolist/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..06952be7 --- /dev/null +++ b/demos/firebase-nodejs-todolist/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/demos/firebase-nodejs-todolist/android/app/src/main/res/values/styles.xml b/demos/firebase-nodejs-todolist/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..cb1ef880 --- /dev/null +++ b/demos/firebase-nodejs-todolist/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/demos/firebase-nodejs-todolist/android/app/src/profile/AndroidManifest.xml b/demos/firebase-nodejs-todolist/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..f19dd7d6 --- /dev/null +++ b/demos/firebase-nodejs-todolist/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + diff --git a/demos/supabase-todolist/android/build.gradle b/demos/firebase-nodejs-todolist/android/build.gradle similarity index 76% rename from demos/supabase-todolist/android/build.gradle rename to demos/firebase-nodejs-todolist/android/build.gradle index 713d7f6e..58d4dcd6 100644 --- a/demos/supabase-todolist/android/build.gradle +++ b/demos/firebase-nodejs-todolist/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.7.10' + ext.kotlin_version = '2.0.0' repositories { google() mavenCentral() @@ -7,6 +7,9 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:7.2.0' + // START: FlutterFire Configuration + classpath 'com.google.gms:google-services:4.3.10' + // END: FlutterFire Configuration classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/demos/firebase-nodejs-todolist/android/gradle.properties b/demos/firebase-nodejs-todolist/android/gradle.properties new file mode 100644 index 00000000..94adc3a3 --- /dev/null +++ b/demos/firebase-nodejs-todolist/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/demos/firebase-nodejs-todolist/android/gradle/wrapper/gradle-wrapper.properties b/demos/firebase-nodejs-todolist/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..9bb05a27 --- /dev/null +++ b/demos/firebase-nodejs-todolist/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip \ No newline at end of file diff --git a/demos/supabase-todolist/android/settings.gradle b/demos/firebase-nodejs-todolist/android/settings.gradle similarity index 100% rename from demos/supabase-todolist/android/settings.gradle rename to demos/firebase-nodejs-todolist/android/settings.gradle diff --git a/demos/firebase-nodejs-todolist/database.sql b/demos/firebase-nodejs-todolist/database.sql new file mode 100644 index 00000000..6d5daaeb --- /dev/null +++ b/demos/firebase-nodejs-todolist/database.sql @@ -0,0 +1,34 @@ +-- Create tables +create table + public.lists ( + id text not null, + created_at timestamp with time zone not null default now(), + name text not null, + owner_id text not null, + constraint lists_pkey primary key (id) + ) tablespace pg_default; + +create table + public.todos ( + id text not null, + created_at timestamp with time zone null default now(), + completed_at timestamp with time zone null, + description text not null, + completed boolean not null default false, + created_by text null, + completed_by text null, + list_id text not null, + constraint todos_pkey primary key (id), + constraint todos_list_id_fkey foreign key (list_id) references lists (id) on delete cascade + ) tablespace pg_default; + +-- Create publication for powersync +create publication powersync for table lists, todos; + +-- Set up Row Level Security (RLS) +-- See https://supabase.com/docs/guides/auth/row-level-security for more details. +alter table public.lists + enable row level security; + +alter table public.todos + enable row level security; diff --git a/demos/firebase-nodejs-todolist/firebase.json b/demos/firebase-nodejs-todolist/firebase.json new file mode 100644 index 00000000..8bad41d3 --- /dev/null +++ b/demos/firebase-nodejs-todolist/firebase.json @@ -0,0 +1 @@ +{"flutter":{"platforms":{"android":{"default":{"projectId":"sample-firebase-ai-app-27d98","appId":"1:1069616552579:android:d2cb390fea186a49db59b6","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"sample-firebase-ai-app-27d98","appId":"1:1069616552579:ios:0d30b90e81427c07db59b6","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"macos":{"default":{"projectId":"sample-firebase-ai-app-27d98","appId":"1:1069616552579:ios:0d30b90e81427c07db59b6","uploadDebugSymbols":false,"fileOutput":"macos/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"sample-firebase-ai-app-27d98","configurations":{"android":"1:1069616552579:android:d2cb390fea186a49db59b6","ios":"1:1069616552579:ios:0d30b90e81427c07db59b6","macos":"1:1069616552579:ios:0d30b90e81427c07db59b6","windows":"1:1069616552579:web:795aadd36a32e2c5db59b6"}}}}}} \ No newline at end of file diff --git a/demos/firebase-nodejs-todolist/ios/.gitignore b/demos/firebase-nodejs-todolist/ios/.gitignore new file mode 100644 index 00000000..7a7f9873 --- /dev/null +++ b/demos/firebase-nodejs-todolist/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/demos/firebase-nodejs-todolist/ios/Flutter/AppFrameworkInfo.plist b/demos/firebase-nodejs-todolist/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 00000000..7c569640 --- /dev/null +++ b/demos/firebase-nodejs-todolist/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/demos/firebase-nodejs-todolist/ios/Flutter/Debug.xcconfig b/demos/firebase-nodejs-todolist/ios/Flutter/Debug.xcconfig new file mode 100644 index 00000000..ec97fc6f --- /dev/null +++ b/demos/firebase-nodejs-todolist/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/demos/firebase-nodejs-todolist/ios/Flutter/Release.xcconfig b/demos/firebase-nodejs-todolist/ios/Flutter/Release.xcconfig new file mode 100644 index 00000000..c4855bfe --- /dev/null +++ b/demos/firebase-nodejs-todolist/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/demos/firebase-nodejs-todolist/ios/Podfile b/demos/firebase-nodejs-todolist/ios/Podfile new file mode 100644 index 00000000..2c1e086a --- /dev/null +++ b/demos/firebase-nodejs-todolist/ios/Podfile @@ -0,0 +1,48 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |project| + flutter_additional_ios_build_settings(project) + end + installer.generated_projects.each do |project| + project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' + end + end + end +end diff --git a/demos/firebase-nodejs-todolist/ios/Podfile.lock b/demos/firebase-nodejs-todolist/ios/Podfile.lock new file mode 100644 index 00000000..f5b33ded --- /dev/null +++ b/demos/firebase-nodejs-todolist/ios/Podfile.lock @@ -0,0 +1,165 @@ +PODS: + - app_links (0.0.2): + - Flutter + - Firebase/Auth (11.10.0): + - Firebase/CoreOnly + - FirebaseAuth (~> 11.10.0) + - Firebase/CoreOnly (11.10.0): + - FirebaseCore (~> 11.10.0) + - firebase_auth (5.5.3): + - Firebase/Auth (= 11.10.0) + - firebase_core + - Flutter + - firebase_core (3.13.0): + - Firebase/CoreOnly (= 11.10.0) + - Flutter + - FirebaseAppCheckInterop (11.15.0) + - FirebaseAuth (11.10.0): + - FirebaseAppCheckInterop (~> 11.0) + - FirebaseAuthInterop (~> 11.0) + - FirebaseCore (~> 11.10.0) + - FirebaseCoreExtension (~> 11.10.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/Environment (~> 8.0) + - GTMSessionFetcher/Core (< 5.0, >= 3.4) + - RecaptchaInterop (~> 101.0) + - FirebaseAuthInterop (11.15.0) + - FirebaseCore (11.10.0): + - FirebaseCoreInternal (~> 11.10.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Logger (~> 8.0) + - FirebaseCoreExtension (11.10.0): + - FirebaseCore (~> 11.10.0) + - FirebaseCoreInternal (11.10.0): + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - Flutter (1.0.0) + - GoogleUtilities/AppDelegateSwizzler (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (8.1.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/Network (8.1.0): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (8.1.0)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.1.0) + - GoogleUtilities/Reachability (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GTMSessionFetcher/Core (4.5.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - powersync-sqlite-core (0.4.5) + - powersync_flutter_libs (0.0.1): + - Flutter + - powersync-sqlite-core (~> 0.4.5) + - RecaptchaInterop (101.0.0) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (3.49.2): + - sqlite3/common (= 3.49.2) + - sqlite3/common (3.49.2) + - sqlite3/dbstatvtab (3.49.2): + - sqlite3/common + - sqlite3/fts5 (3.49.2): + - sqlite3/common + - sqlite3/math (3.49.2): + - sqlite3/common + - sqlite3/perf-threadsafe (3.49.2): + - sqlite3/common + - sqlite3/rtree (3.49.2): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (~> 3.49.1) + - sqlite3/dbstatvtab + - sqlite3/fts5 + - sqlite3/math + - sqlite3/perf-threadsafe + - sqlite3/rtree + - url_launcher_ios (0.0.1): + - Flutter + +DEPENDENCIES: + - app_links (from `.symlinks/plugins/app_links/ios`) + - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) + - firebase_core (from `.symlinks/plugins/firebase_core/ios`) + - Flutter (from `Flutter`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - powersync_flutter_libs (from `.symlinks/plugins/powersync_flutter_libs/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + +SPEC REPOS: + trunk: + - Firebase + - FirebaseAppCheckInterop + - FirebaseAuth + - FirebaseAuthInterop + - FirebaseCore + - FirebaseCoreExtension + - FirebaseCoreInternal + - GoogleUtilities + - GTMSessionFetcher + - powersync-sqlite-core + - RecaptchaInterop + - sqlite3 + +EXTERNAL SOURCES: + app_links: + :path: ".symlinks/plugins/app_links/ios" + firebase_auth: + :path: ".symlinks/plugins/firebase_auth/ios" + firebase_core: + :path: ".symlinks/plugins/firebase_core/ios" + Flutter: + :path: Flutter + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + powersync_flutter_libs: + :path: ".symlinks/plugins/powersync_flutter_libs/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + sqlite3_flutter_libs: + :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + +SPEC CHECKSUMS: + app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7 + Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2 + firebase_auth: 83bf106e5ac670dd3a0af27a86be6cba16a85723 + firebase_core: 2d4534e7b489907dcede540c835b48981d890943 + FirebaseAppCheckInterop: 06fe5a3799278ae4667e6c432edd86b1030fa3df + FirebaseAuth: c4146bdfdc87329f9962babd24dae89373f49a32 + FirebaseAuthInterop: 7087d7a4ee4bc4de019b2d0c240974ed5d89e2fd + FirebaseCore: 8344daef5e2661eb004b177488d6f9f0f24251b7 + FirebaseCoreExtension: 6f357679327f3614e995dc7cf3f2d600bdc774ac + FirebaseCoreInternal: ef4505d2afb1d0ebbc33162cb3795382904b5679 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 + GTMSessionFetcher: fc75fc972958dceedee61cb662ae1da7a83a91cf + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + powersync-sqlite-core: 6f32860379009d2a37cadc9e9427a431bdbd83c8 + powersync_flutter_libs: 7684a62208907328906eb932f1fc8b3d8879974e + RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 + sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + +PODFILE CHECKSUM: 2c1730c97ea13f1ea48b32e9c79de785b4f2f02f + +COCOAPODS: 1.16.2 diff --git a/demos/firebase-nodejs-todolist/ios/Runner.xcodeproj/project.pbxproj b/demos/firebase-nodejs-todolist/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..3960ce08 --- /dev/null +++ b/demos/firebase-nodejs-todolist/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,556 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 5808A50FFFEBFEB63435C2A8 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = FA5376E94CB870616F8A9D3E /* GoogleService-Info.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + B2C70762C97CE3E3CEB912CB /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B9CC0EA1BA15CD3CCAD0356 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 15764CEB058B2B69D5E35280 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 3153F415177CAE497AE7D235 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7B9CC0EA1BA15CD3CCAD0356 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CDF8C9971FE1B0CF3262ED53 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + FA5376E94CB870616F8A9D3E /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B2C70762C97CE3E3CEB912CB /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + A151B04DC3D1415EEF784588 /* Pods */, + C1E97B63847FB6B811E12FEA /* Frameworks */, + FA5376E94CB870616F8A9D3E /* GoogleService-Info.plist */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + A151B04DC3D1415EEF784588 /* Pods */ = { + isa = PBXGroup; + children = ( + 3153F415177CAE497AE7D235 /* Pods-Runner.debug.xcconfig */, + CDF8C9971FE1B0CF3262ED53 /* Pods-Runner.release.xcconfig */, + 15764CEB058B2B69D5E35280 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + C1E97B63847FB6B811E12FEA /* Frameworks */ = { + isa = PBXGroup; + children = ( + 7B9CC0EA1BA15CD3CCAD0356 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + E916CBFE94483EF7C2F17F6C /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 0A5FBCADCBC1AF2E0353A84D /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + 5808A50FFFEBFEB63435C2A8 /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0A5FBCADCBC1AF2E0353A84D /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + E916CBFE94483EF7C2F17F6C /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.powersync.demotodolist; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.powersync.demotodolist; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.powersync.demotodolist; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/demos/firebase-nodejs-todolist/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/demos/firebase-nodejs-todolist/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/demos/firebase-nodejs-todolist/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/demos/firebase-nodejs-todolist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demos/firebase-nodejs-todolist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/demos/firebase-nodejs-todolist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/demos/firebase-nodejs-todolist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/demos/firebase-nodejs-todolist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/demos/firebase-nodejs-todolist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/demos/firebase-nodejs-todolist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demos/firebase-nodejs-todolist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..5e31d3d3 --- /dev/null +++ b/demos/firebase-nodejs-todolist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/firebase-nodejs-todolist/ios/Runner.xcworkspace/contents.xcworkspacedata b/demos/firebase-nodejs-todolist/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/demos/firebase-nodejs-todolist/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/demos/firebase-nodejs-todolist/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demos/firebase-nodejs-todolist/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/demos/firebase-nodejs-todolist/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/demos/firebase-nodejs-todolist/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/demos/firebase-nodejs-todolist/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/demos/firebase-nodejs-todolist/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/demos/firebase-nodejs-todolist/ios/Runner/AppDelegate.swift b/demos/firebase-nodejs-todolist/ios/Runner/AppDelegate.swift new file mode 100644 index 00000000..70693e4a --- /dev/null +++ b/demos/firebase-nodejs-todolist/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d36b1fab --- /dev/null +++ b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 00000000..dc9ada47 Binary files /dev/null and b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 00000000..7353c41e Binary files /dev/null and b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 00000000..797d452e Binary files /dev/null and b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 00000000..6ed2d933 Binary files /dev/null and b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 00000000..4cd7b009 Binary files /dev/null and b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 00000000..fe730945 Binary files /dev/null and b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 00000000..321773cd Binary files /dev/null and b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 00000000..797d452e Binary files /dev/null and b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 00000000..502f463a Binary files /dev/null and b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 00000000..0ec30343 Binary files /dev/null and b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 00000000..0ec30343 Binary files /dev/null and b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 00000000..e9f5fea2 Binary files /dev/null and b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 00000000..84ac32ae Binary files /dev/null and b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 00000000..8953cba0 Binary files /dev/null and b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 00000000..0467bf12 Binary files /dev/null and b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 00000000..0bedcf2f --- /dev/null +++ b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 00000000..89c2725b --- /dev/null +++ b/demos/firebase-nodejs-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/demos/firebase-nodejs-todolist/ios/Runner/Base.lproj/LaunchScreen.storyboard b/demos/firebase-nodejs-todolist/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..f2e259c7 --- /dev/null +++ b/demos/firebase-nodejs-todolist/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/firebase-nodejs-todolist/ios/Runner/Base.lproj/Main.storyboard b/demos/firebase-nodejs-todolist/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f3c28516 --- /dev/null +++ b/demos/firebase-nodejs-todolist/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/firebase-nodejs-todolist/ios/Runner/GoogleService-Info.plist b/demos/firebase-nodejs-todolist/ios/Runner/GoogleService-Info.plist new file mode 100644 index 00000000..87461c99 --- /dev/null +++ b/demos/firebase-nodejs-todolist/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyAOcRgRnbZv_aXm4jukQnLR4YR1nFNL8eQ + GCM_SENDER_ID + 1069616552579 + PLIST_VERSION + 1 + BUNDLE_ID + co.powersync.demotodolist + PROJECT_ID + sample-firebase-ai-app-27d98 + STORAGE_BUCKET + sample-firebase-ai-app-27d98.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:1069616552579:ios:0d30b90e81427c07db59b6 + + \ No newline at end of file diff --git a/demos/firebase-nodejs-todolist/ios/Runner/Info.plist b/demos/firebase-nodejs-todolist/ios/Runner/Info.plist new file mode 100644 index 00000000..b8ee5b63 --- /dev/null +++ b/demos/firebase-nodejs-todolist/ios/Runner/Info.plist @@ -0,0 +1,51 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Powersync Flutter Demo + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + powersync_flutter_demo + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/demos/firebase-nodejs-todolist/ios/Runner/Runner-Bridging-Header.h b/demos/firebase-nodejs-todolist/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 00000000..308a2a56 --- /dev/null +++ b/demos/firebase-nodejs-todolist/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/demos/firebase-nodejs-todolist/ios/firebase_app_id_file.json b/demos/firebase-nodejs-todolist/ios/firebase_app_id_file.json new file mode 100644 index 00000000..c70e242c --- /dev/null +++ b/demos/firebase-nodejs-todolist/ios/firebase_app_id_file.json @@ -0,0 +1,7 @@ +{ + "file_generated_by": "FlutterFire CLI", + "purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory", + "GOOGLE_APP_ID": "1:879921616597:ios:0d0afa4d1d1195410b745c", + "FIREBASE_PROJECT_ID": "kobie-powersync-testing", + "GCM_SENDER_ID": "879921616597" +} \ No newline at end of file diff --git a/demos/firebase-nodejs-todolist/lib/app_config_template.dart b/demos/firebase-nodejs-todolist/lib/app_config_template.dart new file mode 100644 index 00000000..6f558682 --- /dev/null +++ b/demos/firebase-nodejs-todolist/lib/app_config_template.dart @@ -0,0 +1,6 @@ +// Copy this template: `cp lib/app_config_template.dart lib/app_config.dart` +// Edit lib/app_config.dart and enter your details. +class AppConfig { + static const String backendUrl = 'https://foo.supabase.co'; + static const String powersyncUrl = 'https://foo.powersync.journeyapps.com'; +} diff --git a/demos/firebase-nodejs-todolist/lib/firebase.dart b/demos/firebase-nodejs-todolist/lib/firebase.dart new file mode 100644 index 00000000..030efeaf --- /dev/null +++ b/demos/firebase-nodejs-todolist/lib/firebase.dart @@ -0,0 +1,8 @@ +import 'package:firebase_core/firebase_core.dart'; +import 'firebase_options.dart'; + +loadFirebase() async { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); +} diff --git a/demos/firebase-nodejs-todolist/lib/firebase_options.dart b/demos/firebase-nodejs-todolist/lib/firebase_options.dart new file mode 100644 index 00000000..591ea9d6 --- /dev/null +++ b/demos/firebase-nodejs-todolist/lib/firebase_options.dart @@ -0,0 +1,80 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + return macos; + case TargetPlatform.windows: + return windows; + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyDBk2GgaUqLvPWGe6cI0h6G4ZZweS2JKGE', + appId: '1:1069616552579:android:d2cb390fea186a49db59b6', + messagingSenderId: '1069616552579', + projectId: 'sample-firebase-ai-app-27d98', + storageBucket: 'sample-firebase-ai-app-27d98.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyAOcRgRnbZv_aXm4jukQnLR4YR1nFNL8eQ', + appId: '1:1069616552579:ios:0d30b90e81427c07db59b6', + messagingSenderId: '1069616552579', + projectId: 'sample-firebase-ai-app-27d98', + storageBucket: 'sample-firebase-ai-app-27d98.firebasestorage.app', + iosBundleId: 'co.powersync.demotodolist', + ); + + static const FirebaseOptions macos = FirebaseOptions( + apiKey: 'AIzaSyAOcRgRnbZv_aXm4jukQnLR4YR1nFNL8eQ', + appId: '1:1069616552579:ios:0d30b90e81427c07db59b6', + messagingSenderId: '1069616552579', + projectId: 'sample-firebase-ai-app-27d98', + storageBucket: 'sample-firebase-ai-app-27d98.firebasestorage.app', + iosBundleId: 'co.powersync.demotodolist', + ); + + static const FirebaseOptions windows = FirebaseOptions( + apiKey: 'AIzaSyCSsnrrJEu126-EL0MMpLbdmt44nBinONo', + appId: '1:1069616552579:web:795aadd36a32e2c5db59b6', + messagingSenderId: '1069616552579', + projectId: 'sample-firebase-ai-app-27d98', + authDomain: 'sample-firebase-ai-app-27d98.firebaseapp.com', + storageBucket: 'sample-firebase-ai-app-27d98.firebasestorage.app', + ); +} diff --git a/demos/firebase-nodejs-todolist/lib/main.dart b/demos/firebase-nodejs-todolist/lib/main.dart new file mode 100644 index 00000000..8a4bc617 --- /dev/null +++ b/demos/firebase-nodejs-todolist/lib/main.dart @@ -0,0 +1,125 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import './powersync.dart'; + +import './widgets/lists_page.dart'; +import './widgets/login_page.dart'; +import './widgets/query_widget.dart'; +import './widgets/signup_page.dart'; +import './widgets/status_app_bar.dart'; + +void main() async { + // Log info from PowerSync + Logger.root.level = Level.INFO; + Logger.root.onRecord.listen((record) { + if (kDebugMode) { + print( + '[${record.loggerName}] ${record.level.name}: ${record.time}: ${record.message}'); + + if (record.error != null) { + print(record.error); + } + if (record.stackTrace != null) { + print(record.stackTrace); + } + } + }); + + WidgetsFlutterBinding + .ensureInitialized(); //required to get sqlite filepath from path_provider before UI has initialized + await openDatabase(); + final loggedIn = isLoggedIn(); + + runApp(MyApp(loggedIn: loggedIn)); +} + +const defaultQuery = 'SELECT * from todos'; + +const listsPage = ListsPage(); +const homePage = listsPage; + +const sqlConsolePage = Scaffold( + appBar: StatusAppBar(title: Text('SQL Console')), + body: QueryWidget(defaultQuery: defaultQuery)); + +const loginPage = LoginPage(); + +const signupPage = SignupPage(); + +class MyApp extends StatelessWidget { + final bool loggedIn; + + const MyApp({super.key, required this.loggedIn}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'PowerSync Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: loggedIn ? homePage : loginPage); + } +} + +class MyHomePage extends StatelessWidget { + const MyHomePage( + {super.key, + required this.title, + required this.content, + this.floatingActionButton}); + + final String title; + final Widget content; + final Widget? floatingActionButton; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: StatusAppBar(title: Text(title)), + body: Center(child: content), + floatingActionButton: floatingActionButton, + drawer: Drawer( + // Add a ListView to the drawer. This ensures the user can scroll + // through the options in the drawer if there isn't enough vertical + // space to fit everything. + child: ListView( + // Important: Remove any padding from the ListView. + padding: EdgeInsets.zero, + children: [ + const DrawerHeader( + decoration: BoxDecoration( + color: Colors.blue, + ), + child: Text(''), + ), + ListTile( + title: const Text('SQL Console'), + onTap: () { + var navigator = Navigator.of(context); + navigator.pop(); + + navigator.push(MaterialPageRoute( + builder: (context) => sqlConsolePage, + )); + }, + ), + ListTile( + title: const Text('Sign Out'), + onTap: () async { + var navigator = Navigator.of(context); + navigator.pop(); + await logout(); + + navigator.pushReplacement(MaterialPageRoute( + builder: (context) => loginPage, + )); + }, + ), + ], + ), + ), + ); + } +} diff --git a/demos/firebase-nodejs-todolist/lib/models/schema.dart b/demos/firebase-nodejs-todolist/lib/models/schema.dart new file mode 100644 index 00000000..6e18a2e2 --- /dev/null +++ b/demos/firebase-nodejs-todolist/lib/models/schema.dart @@ -0,0 +1,18 @@ +import 'package:powersync/powersync.dart'; + +const schema = Schema(([ + Table('todos', [ + Column.text('list_id'), + Column.text('created_at'), + Column.text('completed_at'), + Column.text('description'), + Column.integer('completed'), + Column.text('created_by'), + Column.text('completed_by'), + ], indexes: [ + // Index to allow efficient lookup within a list + Index('list', [IndexedColumn('list_id')]) + ]), + Table('lists', + [Column.text('created_at'), Column.text('name'), Column.text('owner_id')]) +])); diff --git a/demos/firebase-nodejs-todolist/lib/models/todo_item.dart b/demos/firebase-nodejs-todolist/lib/models/todo_item.dart new file mode 100644 index 00000000..270ff75a --- /dev/null +++ b/demos/firebase-nodejs-todolist/lib/models/todo_item.dart @@ -0,0 +1,38 @@ +import '../powersync.dart'; +import 'package:powersync/sqlite3.dart' as sqlite; + +/// TodoList represents a result row of a query on "todos". +/// +/// This class is immutable - methods on this class do not modify the instance +/// directly. Instead, watch or re-query the data to get the updated item. +class TodoItem { + final String id; + final String description; + final bool completed; + + TodoItem( + {required this.id, required this.description, required this.completed}); + + factory TodoItem.fromRow(sqlite.Row row) { + return TodoItem( + id: row['id'], + description: row['description'], + completed: row['completed'] == 1); + } + + Future toggle() async { + if (completed) { + await db.execute( + 'UPDATE todos SET completed = FALSE, completed_by = NULL, completed_at = NULL WHERE id = ?', + [id]); + } else { + await db.execute( + 'UPDATE todos SET completed = TRUE, completed_by = ?, completed_at = datetime() WHERE id = ?', + [getUserId(), id]); + } + } + + Future delete() async { + await db.execute('DELETE FROM todos WHERE id = ?', [id]); + } +} diff --git a/demos/firebase-nodejs-todolist/lib/models/todo_list.dart b/demos/firebase-nodejs-todolist/lib/models/todo_list.dart new file mode 100644 index 00000000..2964954f --- /dev/null +++ b/demos/firebase-nodejs-todolist/lib/models/todo_list.dart @@ -0,0 +1,93 @@ +import 'package:powersync/sqlite3.dart' as sqlite; + +import './todo_item.dart'; +import '../powersync.dart'; + +/// TodoList represents a result row of a query on "lists". +/// +/// This class is immutable - methods on this class do not modify the instance +/// directly. Instead, watch or re-query the data to get the updated list. +class TodoList { + /// List id (UUID). + final String id; + + /// Descriptive name. + final String name; + + /// Number of completed todos in this list. + final int? completedCount; + + /// Number of pending todos in this list. + final int? pendingCount; + + TodoList( + {required this.id, + required this.name, + this.completedCount, + this.pendingCount}); + + factory TodoList.fromRow(sqlite.Row row) { + return TodoList( + id: row['id'], + name: row['name'], + completedCount: row['completed_count'], + pendingCount: row['pending_count']); + } + + /// Watch all lists. + static Stream> watchLists() { + // This query is automatically re-run when data in "lists" or "todos" is modified. + return db.watch('SELECT * FROM lists ORDER BY created_at').map((results) { + return results.map(TodoList.fromRow).toList(growable: false); + }); + } + + /// Watch all lists, with [completedCount] and [pendingCount] populated. + static Stream> watchListsWithStats() { + // This query is automatically re-run when data in "lists" or "todos" is modified. + return db.watch(''' +SELECT + *, + (SELECT count() FROM todos WHERE list_id = lists.id AND completed = TRUE) as completed_count, + (SELECT count() FROM todos WHERE list_id = lists.id AND completed = FALSE) as pending_count +FROM lists +ORDER BY created_at +''').map((results) { + return results.map(TodoList.fromRow).toList(growable: false); + }); + } + + /// Create a new list + static Future create(String name) async { + final userId = getUserId(); + log.info('userId $userId'); + final results = await db.execute('''INSERT INTO + lists(id, created_at, name, owner_id) + VALUES(uuid(), datetime(), ?, ?) + RETURNING *''', [name, userId]); + return TodoList.fromRow(results.first); + } + + /// Watch items within this list. + Stream> watchItems() { + return db.watch( + 'SELECT * FROM todos WHERE list_id = ? ORDER BY created_at DESC', + parameters: [id]).map((event) { + return event.map(TodoItem.fromRow).toList(growable: false); + }); + } + + /// Delete this list. + Future delete() async { + await db.execute('DELETE FROM lists WHERE id = ?', [id]); + } + + /// Add a new todo item to this list. + Future add(String description) async { + final results = await db.execute('''INSERT INTO + todos(id, created_at, completed, list_id, description, created_by) + VALUES(uuid(), datetime(), FALSE, ?, ?, ?) + RETURNING *''', [id, description, getUserId()]); + return TodoItem.fromRow(results.first); + } +} diff --git a/demos/firebase-nodejs-todolist/lib/powersync.dart b/demos/firebase-nodejs-todolist/lib/powersync.dart new file mode 100644 index 00000000..54da5e90 --- /dev/null +++ b/demos/firebase-nodejs-todolist/lib/powersync.dart @@ -0,0 +1,273 @@ +// This file performs setup of the PowerSync database +import 'dart:convert'; + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:powersync/powersync.dart'; +import 'package:http/http.dart' as http; + +import './app_config.dart'; +import './models/schema.dart'; +import './firebase.dart'; + +final log = Logger('powersync-nodejs'); + +/// Postgres Response codes that we cannot recover from by retrying. +final List fatalResponseCodes = [ + // Class 22 — Data Exception + // Examples include data type mismatch. + RegExp(r'^22...$'), + // Class 23 — Integrity Constraint Violation. + // Examples include NOT NULL, FOREIGN KEY and UNIQUE violations. + RegExp(r'^23...$'), + // INSUFFICIENT PRIVILEGE - typically a row-level security violation + RegExp(r'^42501$'), +]; + +/// Use Custom Node.js backend for authentication and data upload. +class BackendConnector extends PowerSyncBackendConnector { + PowerSyncDatabase db; + //ignore: unused_field + Future? _refreshFuture; + + BackendConnector(this.db); + + /// Get a token to authenticate against the PowerSync instance. + @override + Future fetchCredentials() async { + final user = FirebaseAuth.instance.currentUser; + if (user == null) { + // Not logged in + return null; + } + final idToken = await user.getIdToken(); + + var url = Uri.parse("${AppConfig.backendUrl}/api/auth/token"); + + Map headers = { + 'Authorization': 'Bearer $idToken', + 'Content-Type': 'application/json', // Adjust content-type if needed + }; + + final response = await http.get( + url, + headers: headers, + ); + + if (response.statusCode == 200) { + final body = response.body; + Map parsedBody = jsonDecode(body); + // Use the access token to authenticate against PowerSync + // userId and expiresAt are for debugging purposes only + final expiresAt = parsedBody['expiresAt'] == null + ? null + : DateTime.fromMillisecondsSinceEpoch( + parsedBody['expiresAt']! * 1000); + return PowerSyncCredentials( + endpoint: parsedBody['powerSyncUrl'], + token: parsedBody['token'], + userId: parsedBody['userId'], + expiresAt: expiresAt); + } else { + print('Request failed with status: ${response.statusCode}'); + return null; + } + } + + @override + void invalidateCredentials() { + // Trigger a session refresh if auth fails on PowerSync. + // However, in some cases it can be a while before the session refresh is + // retried. We attempt to trigger the refresh as soon as we get an auth + // failure on PowerSync. + // + // This could happen if the device was offline for a while and the session + // expired, and nothing else attempt to use the session it in the meantime. + // + // Timeout the refresh call to avoid waiting for long retries, + // and ignore any errors. Errors will surface as expired tokens. + } + + // Upload pending changes to Node.js Backend. + @override + Future uploadData(PowerSyncDatabase database) async { + // This function is called whenever there is data to upload, whether the + // device is online or offline. + // If this call throws an error, it is retried periodically. + final transaction = await database.getNextCrudTransaction(); + if (transaction == null) { + return; + } + + CrudEntry? lastOp; + try { + // Note: If transactional consistency is important, use database functions + // or edge functions to process the entire transaction in a single call. + for (var op in transaction.crud) { + lastOp = op; + + var row = Map.of(op.opData!); + row['id'] = op.id; + Map data = {"table": op.table, "data": row}; + if (op.op == UpdateType.put) { + await upsert(data); + } else if (op.op == UpdateType.patch) { + await update(data); + } else if (op.op == UpdateType.delete) { + data = { + "table": op.table, + "data": {"id": op.id} + }; + await delete(data); + } + } + + // All operations successful. + await transaction.complete(); + } on http.ClientException catch (e) { + // Error may be retryable - e.g. network error or temporary server error. + // Throwing an error here causes this call to be retried after a delay. + log.warning('Client exception', e); + rethrow; + } catch (e) { + /// Instead of blocking the queue with these errors, + /// discard the (rest of the) transaction. + /// + /// Note that these errors typically indicate a bug in the application. + /// If protecting against data loss is important, save the failing records + /// elsewhere instead of discarding, and/or notify the user. + log.severe('Data upload error - discarding $lastOp', e); + await transaction.complete(); + } + } +} + +/// Global reference to the database +late final PowerSyncDatabase db; + +upsert(data) async { + var url = Uri.parse("${AppConfig.backendUrl}/api/data"); + + try { + var response = await http.put( + url, + headers: { + 'Content-Type': 'application/json', // Adjust content-type if needed + }, + body: jsonEncode(data), // Encode data to JSON + ); + + if (response.statusCode == 200) { + log.info('PUT request successful: ${response.body}'); + } else { + log.severe('PUT request failed with status: ${response.statusCode}'); + } + } catch (e) { + log.severe('Exception occurred: $e'); + rethrow; + } +} + +update(data) async { + var url = Uri.parse("${AppConfig.backendUrl}/api/data"); + + try { + var response = await http.patch( + url, + headers: { + 'Content-Type': 'application/json', // Adjust content-type if needed + }, + body: jsonEncode(data), // Encode data to JSON + ); + + if (response.statusCode == 200) { + log.info('PUT request successful: ${response.body}'); + } else { + log.severe('PUT request failed with status: ${response.statusCode}'); + } + } catch (e) { + log.severe('Exception occurred: $e'); + rethrow; + } +} + +delete(data) async { + var url = Uri.parse("${AppConfig.backendUrl}/api/data"); + + try { + var response = await http.delete( + url, + headers: { + 'Content-Type': 'application/json', // Adjust content-type if needed + }, + body: jsonEncode(data), // Encode data to JSON + ); + + if (response.statusCode == 200) { + log.info('DELETE request successful: ${response.body}'); + } else { + log.severe('DELETE request failed with status: ${response.statusCode}'); + } + } catch (e) { + log.severe('Exception occurred: $e'); + rethrow; + } +} + +isLoggedIn() { + final user = FirebaseAuth.instance.currentUser; + return user != null; +} + +/// id of the user currently logged in +String? getUserId() { + final user = FirebaseAuth.instance.currentUser; + return user!.uid; +} + +Future getDatabasePath() async { + final dir = await getApplicationSupportDirectory(); + return join(dir.path, 'powersync-demo.db'); +} + +Future openDatabase() async { + // Open the local database + db = PowerSyncDatabase( + schema: schema, + path: await getDatabasePath(), + logger: attachedLogger, + ); + await db.initialize(); + BackendConnector? currentConnector; + + await loadFirebase(); + + final userLoggedIn = isLoggedIn(); + if (userLoggedIn) { + // If the user is already logged in, connect immediately. + // Otherwise, connect once logged in. + currentConnector = BackendConnector(db); + db.connect(connector: currentConnector); + } else { + log.info('User not logged in, setting connection'); + } + + FirebaseAuth.instance.authStateChanges().listen((User? user) async { + if (user != null) { + // Connect to PowerSync when the user is signed in + currentConnector = BackendConnector(db); + db.connect(connector: currentConnector!); + } else { + currentConnector = null; + await db.disconnect(); + } + }); +} + +/// Explicit sign out - clear database and log out. +Future logout() async { + await FirebaseAuth.instance.signOut(); + await db.disconnectAndClear(); +} diff --git a/demos/supabase-todolist-drift/lib/widgets/list_item.dart b/demos/firebase-nodejs-todolist/lib/widgets/list_item.dart similarity index 71% rename from demos/supabase-todolist-drift/lib/widgets/list_item.dart rename to demos/firebase-nodejs-todolist/lib/widgets/list_item.dart index 19c5d798..31290db6 100644 --- a/demos/supabase-todolist-drift/lib/widgets/list_item.dart +++ b/demos/firebase-nodejs-todolist/lib/widgets/list_item.dart @@ -1,30 +1,22 @@ import 'package:flutter/material.dart'; -import 'package:supabase_todolist_drift/database.dart'; -import 'package:supabase_todolist_drift/powersync.dart'; -import 'todo_list_page.dart'; +import './todo_list_page.dart'; +import '../models/todo_list.dart'; class ListItemWidget extends StatelessWidget { ListItemWidget({ required this.list, }) : super(key: ObjectKey(list)); - final ListItemWithStats list; + final TodoList list; Future delete() async { // Server will take care of deleting related todos - await appDb.deleteList(list.self); + await list.delete(); } @override Widget build(BuildContext context) { - viewList() { - var navigator = Navigator.of(context); - - navigator.push(MaterialPageRoute( - builder: (context) => TodoListPage(list: list.self))); - } - final subtext = '${list.pendingCount} pending, ${list.completedCount} completed'; @@ -33,9 +25,12 @@ class ListItemWidget extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ ListTile( - onTap: viewList, + onTap: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => TodoListPage(list: list))); + }, leading: const Icon(Icons.list), - title: Text(list.self.name), + title: Text(list.name), subtitle: Text(subtext)), Row( mainAxisAlignment: MainAxisAlignment.end, diff --git a/demos/supabase-todolist-drift/lib/widgets/list_item_dialog.dart b/demos/firebase-nodejs-todolist/lib/widgets/list_item_dialog.dart similarity index 92% rename from demos/supabase-todolist-drift/lib/widgets/list_item_dialog.dart rename to demos/firebase-nodejs-todolist/lib/widgets/list_item_dialog.dart index 50d0009c..3fb8c133 100644 --- a/demos/supabase-todolist-drift/lib/widgets/list_item_dialog.dart +++ b/demos/firebase-nodejs-todolist/lib/widgets/list_item_dialog.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:supabase_todolist_drift/powersync.dart'; + +import '../models/todo_list.dart'; class ListItemDialog extends StatefulWidget { const ListItemDialog({super.key}); @@ -22,7 +23,7 @@ class _ListItemDialogState extends State { } Future add() async { - await appDb.createList(_textFieldController.text); + await TodoList.create(_textFieldController.text); } @override diff --git a/demos/firebase-nodejs-todolist/lib/widgets/lists_page.dart b/demos/firebase-nodejs-todolist/lib/widgets/lists_page.dart new file mode 100644 index 00000000..aba21fb2 --- /dev/null +++ b/demos/firebase-nodejs-todolist/lib/widgets/lists_page.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; + +import '../powersync.dart'; +import './list_item.dart'; +import './list_item_dialog.dart'; +import '../main.dart'; +import '../models/todo_list.dart'; + +void _showAddDialog(BuildContext context) async { + return showDialog( + context: context, + barrierDismissible: false, // user must tap button! + builder: (BuildContext context) { + return const ListItemDialog(); + }, + ); +} + +class ListsPage extends StatelessWidget { + const ListsPage({super.key}); + + @override + Widget build(BuildContext context) { + const content = ListsWidget(); + + final button = FloatingActionButton( + onPressed: () { + _showAddDialog(context); + }, + tooltip: 'Create List', + child: const Icon(Icons.add), + ); + + final page = MyHomePage( + title: 'Todo Lists', + content: content, + floatingActionButton: button, + ); + return page; + } +} + +class ListsWidget extends StatelessWidget { + const ListsWidget({super.key}); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: db.waitForFirstSync(), + builder: (context, snapshot) { + return switch (snapshot.connectionState) { + ConnectionState.done => StreamBuilder( + stream: TodoList.watchListsWithStats(), + builder: (context, snapshot) { + final items = snapshot.data ?? const []; + + return ListView( + padding: const EdgeInsets.symmetric(vertical: 8.0), + children: items.map((list) { + return ListItemWidget(list: list); + }).toList(), + ); + }, + ), + // waitForFirstSync() did not complete yet + _ => const Text('Busy with sync...'), + }; + }, + ); + } +} diff --git a/demos/supabase-todolist-drift/lib/widgets/login_page.dart b/demos/firebase-nodejs-todolist/lib/widgets/login_page.dart similarity index 88% rename from demos/supabase-todolist-drift/lib/widgets/login_page.dart rename to demos/firebase-nodejs-todolist/lib/widgets/login_page.dart index f54f09da..8c30fd80 100644 --- a/demos/supabase-todolist-drift/lib/widgets/login_page.dart +++ b/demos/firebase-nodejs-todolist/lib/widgets/login_page.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; - +import 'package:firebase_auth/firebase_auth.dart'; import '../main.dart'; class LoginPage extends StatefulWidget { @@ -31,18 +30,23 @@ class _LoginPageState extends State { _error = null; }); try { - await Supabase.instance.client.auth.signInWithPassword( + await FirebaseAuth.instance.signInWithEmailAndPassword( email: _usernameController.text, password: _passwordController.text); - if (context.mounted) { Navigator.of(context).pushReplacement(MaterialPageRoute( builder: (context) => listsPage, )); } - } on AuthException catch (e) { - setState(() { - _error = e.message; - }); + } on FirebaseAuthException catch (e) { + if (e.code == 'user-not-found') { + setState(() { + _error = 'No user found for that email.'; + }); + } else if (e.code == 'wrong-password') { + setState(() { + _error = 'Wrong password provided for that user.'; + }); + } } catch (e) { setState(() { _error = e.toString(); @@ -71,7 +75,7 @@ class _LoginPageState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const Text('Supabase Login'), + const Text('Firebase Email Login'), const SizedBox(height: 35), TextFormField( controller: _usernameController, diff --git a/demos/supabase-todolist-drift/lib/widgets/query_widget.dart b/demos/firebase-nodejs-todolist/lib/widgets/query_widget.dart similarity index 94% rename from demos/supabase-todolist-drift/lib/widgets/query_widget.dart rename to demos/firebase-nodejs-todolist/lib/widgets/query_widget.dart index a3ea9654..b9a285dd 100644 --- a/demos/supabase-todolist-drift/lib/widgets/query_widget.dart +++ b/demos/firebase-nodejs-todolist/lib/widgets/query_widget.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:powersync/sqlite3_common.dart' as sqlite; +import 'package:powersync/sqlite3.dart' as sqlite; -import 'resultset_table.dart'; +import './resultset_table.dart'; import '../powersync.dart'; class QueryWidget extends StatefulWidget { @@ -46,7 +46,7 @@ class QueryWidgetState extends State { _subscription?.cancel(); final stream = db.watch(_query); _subscription = stream.listen((data) { - if (!context.mounted) { + if (!mounted) { return; } setState(() { diff --git a/demos/supabase-todolist-drift/lib/widgets/resultset_table.dart b/demos/firebase-nodejs-todolist/lib/widgets/resultset_table.dart similarity index 94% rename from demos/supabase-todolist-drift/lib/widgets/resultset_table.dart rename to demos/firebase-nodejs-todolist/lib/widgets/resultset_table.dart index f348e4ff..b1606adf 100644 --- a/demos/supabase-todolist-drift/lib/widgets/resultset_table.dart +++ b/demos/firebase-nodejs-todolist/lib/widgets/resultset_table.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:powersync/sqlite3_common.dart' as sqlite; +import 'package:powersync/sqlite3.dart' as sqlite; /// Stateless DataTable rendering results from a SQLite query class ResultSetTable extends StatelessWidget { diff --git a/demos/supabase-todolist-drift/lib/widgets/signup_page.dart b/demos/firebase-nodejs-todolist/lib/widgets/signup_page.dart similarity index 82% rename from demos/supabase-todolist-drift/lib/widgets/signup_page.dart rename to demos/firebase-nodejs-todolist/lib/widgets/signup_page.dart index 2f9150b4..48e7450e 100644 --- a/demos/supabase-todolist-drift/lib/widgets/signup_page.dart +++ b/demos/firebase-nodejs-todolist/lib/widgets/signup_page.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; - +import 'package:firebase_auth/firebase_auth.dart'; import '../main.dart'; class SignupPage extends StatefulWidget { @@ -31,22 +30,25 @@ class _SignupPageState extends State { _error = null; }); try { - final response = await Supabase.instance.client.auth.signUp( - email: _usernameController.text, password: _passwordController.text); - + await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: _usernameController.text, + password: _passwordController.text, + ); if (context.mounted) { - if (response.session != null) { - Navigator.of(context).pushReplacement(MaterialPageRoute( - builder: (context) => homePage, - )); - } else { - Navigator.of(context).pop(); - } + Navigator.of(context).pushReplacement(MaterialPageRoute( + builder: (context) => homePage, + )); + } + } on FirebaseAuthException catch (e) { + if (e.code == 'weak-password') { + setState(() { + _error = 'The password provided is too weak.'; + }); + } else if (e.code == 'email-already-in-use') { + setState(() { + _error = 'The account already exists for that email.'; + }); } - } on AuthException catch (e) { - setState(() { - _error = e.message; - }); } catch (e) { setState(() { _error = e.toString(); diff --git a/demos/firebase-nodejs-todolist/lib/widgets/status_app_bar.dart b/demos/firebase-nodejs-todolist/lib/widgets/status_app_bar.dart new file mode 100644 index 00000000..19c1a025 --- /dev/null +++ b/demos/firebase-nodejs-todolist/lib/widgets/status_app_bar.dart @@ -0,0 +1,67 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:powersync/powersync.dart'; +import '../powersync.dart'; + +class StatusAppBar extends StatelessWidget implements PreferredSizeWidget { + final Widget title; + + const StatusAppBar({super.key, required this.title}); + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: db.statusStream, + initialData: db.currentStatus, + builder: (context, snapshot) { + final status = snapshot.data!; + final statusIcon = _getStatusIcon(status); + + return AppBar( + title: title, + actions: [ + statusIcon, + // Make some space for the "Debug" banner, so that the status + // icon isn't hidden + if (kDebugMode) _makeIcon('Debug mode', Icons.developer_mode), + ], + ); + }, + ); + } +} + +Widget _makeIcon(String text, IconData icon) { + return Tooltip( + message: text, + child: SizedBox(width: 40, height: null, child: Icon(icon, size: 24))); +} + +Widget _getStatusIcon(SyncStatus status) { + if (status.anyError != null) { + // The error message is verbose, could be replaced with something + // more user-friendly + if (!status.connected) { + return _makeIcon(status.anyError!.toString(), Icons.cloud_off); + } else { + return _makeIcon(status.anyError!.toString(), Icons.sync_problem); + } + } else if (status.connecting) { + return _makeIcon('Connecting', Icons.cloud_sync_outlined); + } else if (!status.connected) { + return _makeIcon('Not connected', Icons.cloud_off); + } else if (status.uploading && status.downloading) { + // The status changes often between downloading, uploading and both, + // so we use the same icon for all three + return _makeIcon('Uploading and downloading', Icons.cloud_sync_outlined); + } else if (status.uploading) { + return _makeIcon('Uploading', Icons.cloud_sync_outlined); + } else if (status.downloading) { + return _makeIcon('Downloading', Icons.cloud_sync_outlined); + } else { + return _makeIcon('Connected', Icons.cloud_queue); + } +} diff --git a/demos/supabase-todolist-drift/lib/widgets/todo_item_dialog.dart b/demos/firebase-nodejs-todolist/lib/widgets/todo_item_dialog.dart similarity index 86% rename from demos/supabase-todolist-drift/lib/widgets/todo_item_dialog.dart rename to demos/firebase-nodejs-todolist/lib/widgets/todo_item_dialog.dart index 269fcf65..641abd7f 100644 --- a/demos/supabase-todolist-drift/lib/widgets/todo_item_dialog.dart +++ b/demos/firebase-nodejs-todolist/lib/widgets/todo_item_dialog.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:supabase_todolist_drift/database.dart'; -import 'package:supabase_todolist_drift/powersync.dart'; + +import '../models/todo_list.dart'; class TodoItemDialog extends StatefulWidget { - final ListItem list; + final TodoList list; const TodoItemDialog({super.key, required this.list}); @@ -32,7 +32,7 @@ class _TodoItemDialogState extends State { Future add() async { Navigator.of(context).pop(); - await appDb.addTodo(widget.list, _textFieldController.text); + await widget.list.add(_textFieldController.text); } @override diff --git a/demos/supabase-todolist-drift/lib/widgets/todo_item_widget.dart b/demos/firebase-nodejs-todolist/lib/widgets/todo_item_widget.dart similarity index 53% rename from demos/supabase-todolist-drift/lib/widgets/todo_item_widget.dart rename to demos/firebase-nodejs-todolist/lib/widgets/todo_item_widget.dart index 374e9e6f..2bb6050c 100644 --- a/demos/supabase-todolist-drift/lib/widgets/todo_item_widget.dart +++ b/demos/firebase-nodejs-todolist/lib/widgets/todo_item_widget.dart @@ -1,9 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:supabase_todolist_drift/app_config.dart'; -import 'package:supabase_todolist_drift/attachments/photo_widget.dart'; -import 'package:supabase_todolist_drift/attachments/queue.dart'; -import 'package:supabase_todolist_drift/database.dart'; -import 'package:supabase_todolist_drift/powersync.dart'; + +import '../models/todo_item.dart'; class TodoItemWidget extends StatelessWidget { TodoItemWidget({ @@ -21,28 +18,21 @@ class TodoItemWidget extends StatelessWidget { ); } - Future deleteTodo(TodoItem todo) async { - if (todo.photoId != null) { - attachmentQueue.deleteFile(todo.photoId!); - } - await appDb.deleteTodo(todo); - } - @override Widget build(BuildContext context) { return ListTile( - onTap: () => appDb.toggleTodo(todo), + onTap: todo.toggle, leading: Checkbox( value: todo.completed, onChanged: (_) { - appDb.toggleTodo(todo); + todo.toggle(); }, ), title: Row( children: [ Expanded( child: Text(todo.description, - style: _getTextStyle(todo.completed == true))), + style: _getTextStyle(todo.completed))), IconButton( iconSize: 30, icon: const Icon( @@ -50,12 +40,9 @@ class TodoItemWidget extends StatelessWidget { color: Colors.red, ), alignment: Alignment.centerRight, - onPressed: () async => await deleteTodo(todo), + onPressed: todo.delete, tooltip: 'Delete Item', - ), - AppConfig.supabaseStorageBucket.isEmpty - ? Container() - : PhotoWidget(todo: todo), + ) ], )); } diff --git a/demos/firebase-nodejs-todolist/lib/widgets/todo_list_page.dart b/demos/firebase-nodejs-todolist/lib/widgets/todo_list_page.dart new file mode 100644 index 00000000..a457eb98 --- /dev/null +++ b/demos/firebase-nodejs-todolist/lib/widgets/todo_list_page.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; + +import './status_app_bar.dart'; +import './todo_item_dialog.dart'; +import './todo_item_widget.dart'; +import '../models/todo_list.dart'; + +void _showAddDialog(BuildContext context, TodoList list) async { + return showDialog( + context: context, + barrierDismissible: false, // user must tap button! + builder: (BuildContext context) { + return TodoItemDialog(list: list); + }, + ); +} + +class TodoListPage extends StatelessWidget { + final TodoList list; + + const TodoListPage({super.key, required this.list}); + + @override + Widget build(BuildContext context) { + final button = FloatingActionButton( + onPressed: () { + _showAddDialog(context, list); + }, + tooltip: 'Add Item', + child: const Icon(Icons.add), + ); + + return Scaffold( + appBar: StatusAppBar(title: Text(list.name)), + floatingActionButton: button, + body: TodoListWidget(list: list)); + } +} + +class TodoListWidget extends StatelessWidget { + final TodoList list; + + const TodoListWidget({super.key, required this.list}); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: list.watchItems(), + builder: (context, snapshot) { + final items = snapshot.data ?? const []; + + return ListView( + padding: const EdgeInsets.symmetric(vertical: 8.0), + children: items.map((todo) { + return TodoItemWidget(todo: todo); + }).toList(), + ); + }, + ); + } +} diff --git a/demos/firebase-nodejs-todolist/linux/.gitignore b/demos/firebase-nodejs-todolist/linux/.gitignore new file mode 100644 index 00000000..d3896c98 --- /dev/null +++ b/demos/firebase-nodejs-todolist/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/demos/firebase-nodejs-todolist/linux/CMakeLists.txt b/demos/firebase-nodejs-todolist/linux/CMakeLists.txt new file mode 100644 index 00000000..df0a3887 --- /dev/null +++ b/demos/firebase-nodejs-todolist/linux/CMakeLists.txt @@ -0,0 +1,138 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "powersync_supabase_demo") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "co.powersync.demotodolist") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/demos/firebase-nodejs-todolist/linux/flutter/CMakeLists.txt b/demos/firebase-nodejs-todolist/linux/flutter/CMakeLists.txt new file mode 100644 index 00000000..d5bd0164 --- /dev/null +++ b/demos/firebase-nodejs-todolist/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/demos/firebase-nodejs-todolist/linux/flutter/generated_plugin_registrant.cc b/demos/firebase-nodejs-todolist/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..1bef6a30 --- /dev/null +++ b/demos/firebase-nodejs-todolist/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,27 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); + g_autoptr(FlPluginRegistrar) powersync_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "PowersyncFlutterLibsPlugin"); + powersync_flutter_libs_plugin_register_with_registrar(powersync_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); + sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/demos/firebase-nodejs-todolist/linux/flutter/generated_plugin_registrant.h b/demos/firebase-nodejs-todolist/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..e0f0a47b --- /dev/null +++ b/demos/firebase-nodejs-todolist/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/demos/firebase-nodejs-todolist/linux/flutter/generated_plugins.cmake b/demos/firebase-nodejs-todolist/linux/flutter/generated_plugins.cmake new file mode 100644 index 00000000..ed77a1a0 --- /dev/null +++ b/demos/firebase-nodejs-todolist/linux/flutter/generated_plugins.cmake @@ -0,0 +1,27 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + gtk + powersync_flutter_libs + sqlite3_flutter_libs + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/demos/firebase-nodejs-todolist/linux/main.cc b/demos/firebase-nodejs-todolist/linux/main.cc new file mode 100644 index 00000000..e7c5c543 --- /dev/null +++ b/demos/firebase-nodejs-todolist/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/demos/firebase-nodejs-todolist/linux/my_application.cc b/demos/firebase-nodejs-todolist/linux/my_application.cc new file mode 100644 index 00000000..7dcb7e37 --- /dev/null +++ b/demos/firebase-nodejs-todolist/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "PowerSync Flutter Demo"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "PowerSync Flutter Demo"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/demos/firebase-nodejs-todolist/linux/my_application.h b/demos/firebase-nodejs-todolist/linux/my_application.h new file mode 100644 index 00000000..72271d5e --- /dev/null +++ b/demos/firebase-nodejs-todolist/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/demos/firebase-nodejs-todolist/macos/.gitignore b/demos/firebase-nodejs-todolist/macos/.gitignore new file mode 100644 index 00000000..746adbb6 --- /dev/null +++ b/demos/firebase-nodejs-todolist/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/demos/firebase-nodejs-todolist/macos/Flutter/Flutter-Debug.xcconfig b/demos/firebase-nodejs-todolist/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 00000000..4b81f9b2 --- /dev/null +++ b/demos/firebase-nodejs-todolist/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/demos/firebase-nodejs-todolist/macos/Flutter/Flutter-Release.xcconfig b/demos/firebase-nodejs-todolist/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 00000000..5caa9d15 --- /dev/null +++ b/demos/firebase-nodejs-todolist/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/demos/firebase-nodejs-todolist/macos/Flutter/GeneratedPluginRegistrant.swift b/demos/firebase-nodejs-todolist/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 00000000..e113183e --- /dev/null +++ b/demos/firebase-nodejs-todolist/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,26 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import app_links +import firebase_auth +import firebase_core +import path_provider_foundation +import powersync_flutter_libs +import shared_preferences_foundation +import sqlite3_flutter_libs +import url_launcher_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) + FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + PowersyncFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "PowersyncFlutterLibsPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) +} diff --git a/demos/firebase-nodejs-todolist/macos/Podfile b/demos/firebase-nodejs-todolist/macos/Podfile new file mode 100644 index 00000000..b52666a1 --- /dev/null +++ b/demos/firebase-nodejs-todolist/macos/Podfile @@ -0,0 +1,43 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/demos/firebase-nodejs-todolist/macos/Podfile.lock b/demos/firebase-nodejs-todolist/macos/Podfile.lock new file mode 100644 index 00000000..fc4bb3aa --- /dev/null +++ b/demos/firebase-nodejs-todolist/macos/Podfile.lock @@ -0,0 +1,163 @@ +PODS: + - app_links (1.0.0): + - FlutterMacOS + - Firebase/Auth (11.10.0): + - Firebase/CoreOnly + - FirebaseAuth (~> 11.10.0) + - Firebase/CoreOnly (11.10.0): + - FirebaseCore (~> 11.10.0) + - firebase_auth (5.5.3): + - Firebase/Auth (~> 11.10.0) + - Firebase/CoreOnly (~> 11.10.0) + - firebase_core + - FlutterMacOS + - firebase_core (3.13.0): + - Firebase/CoreOnly (~> 11.10.0) + - FlutterMacOS + - FirebaseAppCheckInterop (11.15.0) + - FirebaseAuth (11.10.0): + - FirebaseAppCheckInterop (~> 11.0) + - FirebaseAuthInterop (~> 11.0) + - FirebaseCore (~> 11.10.0) + - FirebaseCoreExtension (~> 11.10.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/Environment (~> 8.0) + - GTMSessionFetcher/Core (< 5.0, >= 3.4) + - RecaptchaInterop (~> 101.0) + - FirebaseAuthInterop (11.15.0) + - FirebaseCore (11.10.0): + - FirebaseCoreInternal (~> 11.10.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Logger (~> 8.0) + - FirebaseCoreExtension (11.10.0): + - FirebaseCore (~> 11.10.0) + - FirebaseCoreInternal (11.10.0): + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - FlutterMacOS (1.0.0) + - GoogleUtilities/AppDelegateSwizzler (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (8.1.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/Network (8.1.0): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (8.1.0)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.1.0) + - GoogleUtilities/Reachability (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GTMSessionFetcher/Core (4.5.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - powersync-sqlite-core (0.4.5) + - powersync_flutter_libs (0.0.1): + - FlutterMacOS + - powersync-sqlite-core (~> 0.4.5) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (3.49.2): + - sqlite3/common (= 3.49.2) + - sqlite3/common (3.49.2) + - sqlite3/dbstatvtab (3.49.2): + - sqlite3/common + - sqlite3/fts5 (3.49.2): + - sqlite3/common + - sqlite3/math (3.49.2): + - sqlite3/common + - sqlite3/perf-threadsafe (3.49.2): + - sqlite3/common + - sqlite3/rtree (3.49.2): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (~> 3.49.1) + - sqlite3/dbstatvtab + - sqlite3/fts5 + - sqlite3/math + - sqlite3/perf-threadsafe + - sqlite3/rtree + - url_launcher_macos (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) + - firebase_auth (from `Flutter/ephemeral/.symlinks/plugins/firebase_auth/macos`) + - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - powersync_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/powersync_flutter_libs/macos`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + +SPEC REPOS: + trunk: + - Firebase + - FirebaseAppCheckInterop + - FirebaseAuth + - FirebaseAuthInterop + - FirebaseCore + - FirebaseCoreExtension + - FirebaseCoreInternal + - GoogleUtilities + - GTMSessionFetcher + - powersync-sqlite-core + - sqlite3 + +EXTERNAL SOURCES: + app_links: + :path: Flutter/ephemeral/.symlinks/plugins/app_links/macos + firebase_auth: + :path: Flutter/ephemeral/.symlinks/plugins/firebase_auth/macos + firebase_core: + :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos + FlutterMacOS: + :path: Flutter/ephemeral + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + powersync_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/powersync_flutter_libs/macos + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + sqlite3_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + +SPEC CHECKSUMS: + app_links: afe860c55c7ef176cea7fb630a2b7d7736de591d + Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2 + firebase_auth: d9a868727e64a42540f791ffb5a656afa0c29a58 + firebase_core: efd50ad8177dc489af1b9163a560359cf1b30597 + FirebaseAppCheckInterop: 06fe5a3799278ae4667e6c432edd86b1030fa3df + FirebaseAuth: c4146bdfdc87329f9962babd24dae89373f49a32 + FirebaseAuthInterop: 7087d7a4ee4bc4de019b2d0c240974ed5d89e2fd + FirebaseCore: 8344daef5e2661eb004b177488d6f9f0f24251b7 + FirebaseCoreExtension: 6f357679327f3614e995dc7cf3f2d600bdc774ac + FirebaseCoreInternal: ef4505d2afb1d0ebbc33162cb3795382904b5679 + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 + GTMSessionFetcher: fc75fc972958dceedee61cb662ae1da7a83a91cf + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + powersync-sqlite-core: 6f32860379009d2a37cadc9e9427a431bdbd83c8 + powersync_flutter_libs: 41d8a7b193abf15e46f95f0ec1229d86b6893171 + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 + sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 + +PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 + +COCOAPODS: 1.16.2 diff --git a/demos/firebase-nodejs-todolist/macos/Runner.xcodeproj/project.pbxproj b/demos/firebase-nodejs-todolist/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..2ab704c4 --- /dev/null +++ b/demos/firebase-nodejs-todolist/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,838 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 04EE2EEA1AF4432FCFE4D947 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 386AF35B349F70B5D676F5EC /* Pods_Runner.framework */; }; + 2F56F886B3B1884D3E437FD0 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C2FC729F34600C40853A030B /* Pods_RunnerTests.framework */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 8B5261612A7C463D00E9899E /* powersync_flutter_demoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B5261602A7C463D00E9899E /* powersync_flutter_demoTests.swift */; }; + 9DF5AEE5CF21BB7D19AEF276 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = AF177A3BDF03AB0312548FD9 /* GoogleService-Info.plist */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; + 8B5261622A7C463D00E9899E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1FB90A99EA939D06EE287C09 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* powersync_flutter_demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = powersync_flutter_demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 386AF35B349F70B5D676F5EC /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 859D7659433CF3D1320F86CC /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 8B52615E2A7C463D00E9899E /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 8B5261602A7C463D00E9899E /* powersync_flutter_demoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = powersync_flutter_demoTests.swift; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + 9DCB9EDE28DF57E29440CF22 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + AF177A3BDF03AB0312548FD9 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; + AF676D80A0CF80705DF388CF /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + C1A05183B57D5869377A17B4 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + C2FC729F34600C40853A030B /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0A6A6185A7A65698B8F4B1D /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 04EE2EEA1AF4432FCFE4D947 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8B52615B2A7C463D00E9899E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2F56F886B3B1884D3E437FD0 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 8B52615F2A7C463D00E9899E /* powersync_flutter_demoTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + B6C445B3E9905835336FDF92 /* Pods */, + AF177A3BDF03AB0312548FD9 /* GoogleService-Info.plist */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* powersync_flutter_demo.app */, + 8B52615E2A7C463D00E9899E /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 8B52615F2A7C463D00E9899E /* powersync_flutter_demoTests */ = { + isa = PBXGroup; + children = ( + 8B5261602A7C463D00E9899E /* powersync_flutter_demoTests.swift */, + ); + path = powersync_flutter_demoTests; + sourceTree = ""; + }; + B6C445B3E9905835336FDF92 /* Pods */ = { + isa = PBXGroup; + children = ( + 1FB90A99EA939D06EE287C09 /* Pods-Runner.debug.xcconfig */, + AF676D80A0CF80705DF388CF /* Pods-Runner.release.xcconfig */, + C1A05183B57D5869377A17B4 /* Pods-Runner.profile.xcconfig */, + 9DCB9EDE28DF57E29440CF22 /* Pods-RunnerTests.debug.xcconfig */, + D0A6A6185A7A65698B8F4B1D /* Pods-RunnerTests.release.xcconfig */, + 859D7659433CF3D1320F86CC /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 386AF35B349F70B5D676F5EC /* Pods_Runner.framework */, + C2FC729F34600C40853A030B /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9E5C59BA43BACEF39908FBDE /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 44F34942EBFBB7F6E89ED4BA /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* powersync_flutter_demo.app */; + productType = "com.apple.product-type.application"; + }; + 8B52615D2A7C463D00E9899E /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8B5261672A7C463D00E9899E /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 9BD4CD7B4DFE9A6CC5BE206C /* [CP] Check Pods Manifest.lock */, + 8B52615A2A7C463D00E9899E /* Sources */, + 8B52615B2A7C463D00E9899E /* Frameworks */, + 8B52615C2A7C463D00E9899E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 8B5261632A7C463D00E9899E /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = powersync_flutter_demoTests; + productReference = 8B52615E2A7C463D00E9899E /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1430; + LastUpgradeCheck = 1430; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + 8B52615D2A7C463D00E9899E = { + CreatedOnToolsVersion = 14.3.1; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + 8B52615D2A7C463D00E9899E /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + 9DF5AEE5CF21BB7D19AEF276 /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8B52615C2A7C463D00E9899E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 44F34942EBFBB7F6E89ED4BA /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9BD4CD7B4DFE9A6CC5BE206C /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9E5C59BA43BACEF39908FBDE /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8B52615A2A7C463D00E9899E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8B5261612A7C463D00E9899E /* powersync_flutter_demoTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; + 8B5261632A7C463D00E9899E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 8B5261622A7C463D00E9899E /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + 8B5261642A7C463D00E9899E /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9DCB9EDE28DF57E29440CF22 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 13.3; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "co.powersync.flutter-todolist-demo.powersync-flutter-demoTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/powersync_flutter_demo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/powersync_flutter_demo"; + }; + name = Debug; + }; + 8B5261652A7C463D00E9899E /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0A6A6185A7A65698B8F4B1D /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 13.3; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "co.powersync.flutter-todolist-demo.powersync-flutter-demoTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/powersync_flutter_demo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/powersync_flutter_demo"; + }; + name = Release; + }; + 8B5261662A7C463D00E9899E /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 859D7659433CF3D1320F86CC /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 13.3; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "co.powersync.flutter-todolist-demo.powersync-flutter-demoTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/powersync_flutter_demo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/powersync_flutter_demo"; + }; + name = Profile; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8B5261672A7C463D00E9899E /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8B5261642A7C463D00E9899E /* Debug */, + 8B5261652A7C463D00E9899E /* Release */, + 8B5261662A7C463D00E9899E /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/demos/firebase-nodejs-todolist/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demos/firebase-nodejs-todolist/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/demos/firebase-nodejs-todolist/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/demos/firebase-nodejs-todolist/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demos/firebase-nodejs-todolist/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..cc0e03af --- /dev/null +++ b/demos/firebase-nodejs-todolist/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/firebase-nodejs-todolist/macos/Runner.xcworkspace/contents.xcworkspacedata b/demos/firebase-nodejs-todolist/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/demos/firebase-nodejs-todolist/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/demos/firebase-nodejs-todolist/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demos/firebase-nodejs-todolist/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/demos/firebase-nodejs-todolist/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/demos/firebase-nodejs-todolist/macos/Runner/AppDelegate.swift b/demos/firebase-nodejs-todolist/macos/Runner/AppDelegate.swift new file mode 100644 index 00000000..d53ef643 --- /dev/null +++ b/demos/firebase-nodejs-todolist/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/demos/firebase-nodejs-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/demos/firebase-nodejs-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..a2ec33f1 --- /dev/null +++ b/demos/firebase-nodejs-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/demos/firebase-nodejs-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/demos/firebase-nodejs-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 00000000..82b6f9d9 Binary files /dev/null and b/demos/firebase-nodejs-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/demos/firebase-nodejs-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/demos/firebase-nodejs-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 00000000..13b35eba Binary files /dev/null and b/demos/firebase-nodejs-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/demos/firebase-nodejs-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/demos/firebase-nodejs-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 00000000..0a3f5fa4 Binary files /dev/null and b/demos/firebase-nodejs-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/demos/firebase-nodejs-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/demos/firebase-nodejs-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 00000000..bdb57226 Binary files /dev/null and b/demos/firebase-nodejs-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/demos/firebase-nodejs-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/demos/firebase-nodejs-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 00000000..f083318e Binary files /dev/null and b/demos/firebase-nodejs-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/demos/firebase-nodejs-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/demos/firebase-nodejs-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 00000000..326c0e72 Binary files /dev/null and b/demos/firebase-nodejs-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/demos/firebase-nodejs-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/demos/firebase-nodejs-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 00000000..2f1632cf Binary files /dev/null and b/demos/firebase-nodejs-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/demos/firebase-nodejs-todolist/macos/Runner/Base.lproj/MainMenu.xib b/demos/firebase-nodejs-todolist/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 00000000..80e867a4 --- /dev/null +++ b/demos/firebase-nodejs-todolist/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/firebase-nodejs-todolist/macos/Runner/Configs/AppInfo.xcconfig b/demos/firebase-nodejs-todolist/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 00000000..370c9893 --- /dev/null +++ b/demos/firebase-nodejs-todolist/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = PowerSync Supabase Demo + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = co.powersync.demotodolist + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2023 Journey Mobile, Inc. All rights reserved. diff --git a/demos/firebase-nodejs-todolist/macos/Runner/Configs/Debug.xcconfig b/demos/firebase-nodejs-todolist/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 00000000..36b0fd94 --- /dev/null +++ b/demos/firebase-nodejs-todolist/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/demos/firebase-nodejs-todolist/macos/Runner/Configs/Release.xcconfig b/demos/firebase-nodejs-todolist/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 00000000..dff4f495 --- /dev/null +++ b/demos/firebase-nodejs-todolist/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/demos/firebase-nodejs-todolist/macos/Runner/Configs/Warnings.xcconfig b/demos/firebase-nodejs-todolist/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 00000000..42bcbf47 --- /dev/null +++ b/demos/firebase-nodejs-todolist/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/demos/firebase-nodejs-todolist/macos/Runner/DebugProfile.entitlements b/demos/firebase-nodejs-todolist/macos/Runner/DebugProfile.entitlements new file mode 100644 index 00000000..08c3ab17 --- /dev/null +++ b/demos/firebase-nodejs-todolist/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + + diff --git a/demos/firebase-nodejs-todolist/macos/Runner/GoogleService-Info.plist b/demos/firebase-nodejs-todolist/macos/Runner/GoogleService-Info.plist new file mode 100644 index 00000000..87461c99 --- /dev/null +++ b/demos/firebase-nodejs-todolist/macos/Runner/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyAOcRgRnbZv_aXm4jukQnLR4YR1nFNL8eQ + GCM_SENDER_ID + 1069616552579 + PLIST_VERSION + 1 + BUNDLE_ID + co.powersync.demotodolist + PROJECT_ID + sample-firebase-ai-app-27d98 + STORAGE_BUCKET + sample-firebase-ai-app-27d98.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:1069616552579:ios:0d30b90e81427c07db59b6 + + \ No newline at end of file diff --git a/demos/firebase-nodejs-todolist/macos/Runner/Info.plist b/demos/firebase-nodejs-todolist/macos/Runner/Info.plist new file mode 100644 index 00000000..4789daa6 --- /dev/null +++ b/demos/firebase-nodejs-todolist/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/demos/firebase-nodejs-todolist/macos/Runner/MainFlutterWindow.swift b/demos/firebase-nodejs-todolist/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 00000000..2722837e --- /dev/null +++ b/demos/firebase-nodejs-todolist/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/demos/firebase-nodejs-todolist/macos/Runner/Release.entitlements b/demos/firebase-nodejs-todolist/macos/Runner/Release.entitlements new file mode 100644 index 00000000..ee95ab7e --- /dev/null +++ b/demos/firebase-nodejs-todolist/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/demos/firebase-nodejs-todolist/macos/firebase_app_id_file.json b/demos/firebase-nodejs-todolist/macos/firebase_app_id_file.json new file mode 100644 index 00000000..c70e242c --- /dev/null +++ b/demos/firebase-nodejs-todolist/macos/firebase_app_id_file.json @@ -0,0 +1,7 @@ +{ + "file_generated_by": "FlutterFire CLI", + "purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory", + "GOOGLE_APP_ID": "1:879921616597:ios:0d0afa4d1d1195410b745c", + "FIREBASE_PROJECT_ID": "kobie-powersync-testing", + "GCM_SENDER_ID": "879921616597" +} \ No newline at end of file diff --git a/demos/firebase-nodejs-todolist/pubspec.lock b/demos/firebase-nodejs-todolist/pubspec.lock new file mode 100644 index 00000000..5eead021 --- /dev/null +++ b/demos/firebase-nodejs-todolist/pubspec.lock @@ -0,0 +1,815 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: de9ecbb3ddafd446095f7e833c853aff2fa1682b017921fe63a833f9d6f0e422 + url: "https://pub.dev" + source: hosted + version: "1.3.54" + app_links: + dependency: transitive + description: + name: app_links + sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba" + url: "https://pub.dev" + source: hosted + version: "6.4.0" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + firebase_auth: + dependency: "direct main" + description: + name: firebase_auth + sha256: "06787c45d773af3db3ae693ff648ef488e6048a00b654620b3b8849988f63793" + url: "https://pub.dev" + source: hosted + version: "5.5.3" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + sha256: "5402d13f4bb7f29f2fb819f3b6b5a5a56c9f714aef2276546d397e25ac1b6b8e" + url: "https://pub.dev" + source: hosted + version: "7.6.2" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + sha256: "2be496911f0807895d5fe8067b70b7d758142dd7fb26485cbe23e525e2547764" + url: "https://pub.dev" + source: hosted + version: "5.14.2" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "017d17d9915670e6117497e640b2859e0b868026ea36bf3a57feb28c3b97debe" + url: "https://pub.dev" + source: hosted + version: "3.13.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf + url: "https://pub.dev" + source: hosted + version: "5.4.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "129a34d1e0fb62e2b488d988a1fc26cc15636357e50944ffee2862efe8929b23" + url: "https://pub.dev" + source: hosted + version: "2.22.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + functions_client: + dependency: transitive + description: + name: functions_client + sha256: b410e4d609522357396cd84bb9a8f6e3a4561b5f7d3ce82267f6f1c2af42f16b + url: "https://pub.dev" + source: hosted + version: "2.4.2" + gotrue: + dependency: transitive + description: + name: gotrue + sha256: "04a6efacffd42773ed96dc752f19bb20a1fbc383e81ba82659072b775cf62912" + url: "https://pub.dev" + source: hosted + version: "2.12.0" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" + http: + dependency: "direct main" + description: + name: http + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + jwt_decode: + dependency: transitive + description: + name: jwt_decode + sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb + url: "https://pub.dev" + source: hosted + version: "0.3.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + url: "https://pub.dev" + source: hosted + version: "10.0.9" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.dev" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + logging: + dependency: "direct main" + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mutex: + dependency: transitive + description: + name: mutex + sha256: "8827da25de792088eb33e572115a5eb0d61d61a3c01acbc8bcbe76ed78f1a1f2" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + path: + dependency: "direct main" + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + url: "https://pub.dev" + source: hosted + version: "2.2.17" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + postgrest: + dependency: transitive + description: + name: postgrest + sha256: "10b81a23b1c829ccadf68c626b4d66666453a1474d24c563f313f5ca7851d575" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + powersync: + dependency: "direct main" + description: + path: "../../packages/powersync" + relative: true + source: path + version: "1.15.0" + powersync_core: + dependency: "direct overridden" + description: + path: "../../packages/powersync_core" + relative: true + source: path + version: "1.5.0" + powersync_flutter_libs: + dependency: "direct overridden" + description: + path: "../../packages/powersync_flutter_libs" + relative: true + source: path + version: "0.4.10" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + realtime_client: + dependency: transitive + description: + name: realtime_client + sha256: "3a0a99b5bd0fc3b35e8ee846d9a22fa2c2117f7ef1cb73d1e5f08f6c3d09c4e9" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + retry: + dependency: transitive + description: + name: retry + sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" + url: "https://pub.dev" + source: hosted + version: "2.4.10" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e" + url: "https://pub.dev" + source: hosted + version: "2.7.5" + sqlite3_flutter_libs: + dependency: transitive + description: + name: sqlite3_flutter_libs + sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14" + url: "https://pub.dev" + source: hosted + version: "0.5.32" + sqlite3_web: + dependency: transitive + description: + name: sqlite3_web + sha256: "967e076442f7e1233bd7241ca61f3efe4c7fc168dac0f38411bdb3bdf471eb3c" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + sqlite_async: + dependency: transitive + description: + name: sqlite_async + sha256: a60e8d5c8df8e694933bd5a312c38393e79ad77d784bb91c6f38ba627bfb7aec + url: "https://pub.dev" + source: hosted + version: "0.11.4" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + storage_client: + dependency: transitive + description: + name: storage_client + sha256: "09bac4d75eea58e8113ca928e6655a09cc8059e6d1b472ee801f01fde815bcfc" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + supabase: + dependency: transitive + description: + name: supabase + sha256: f00172f5f0b2148ea1c573f52862d50cacb6f353f579f741fa35e51704845958 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + supabase_flutter: + dependency: "direct main" + description: + name: supabase_flutter + sha256: d88eccf9e46e57129725a08e72a3109b6f780921fdc27fe3d7669a11ae80906b + url: "https://pub.dev" + source: hosted + version: "2.9.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.dev" + source: hosted + version: "0.7.4" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" + url: "https://pub.dev" + source: hosted + version: "6.3.16" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" + url: "https://pub.dev" + source: hosted + version: "6.3.3" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + url: "https://pub.dev" + source: hosted + version: "15.0.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" + yet_another_json_isolate: + dependency: transitive + description: + name: yet_another_json_isolate + sha256: fe45897501fa156ccefbfb9359c9462ce5dec092f05e8a56109db30be864f01e + url: "https://pub.dev" + source: hosted + version: "2.1.0" +sdks: + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/demos/firebase-nodejs-todolist/pubspec.yaml b/demos/firebase-nodejs-todolist/pubspec.yaml new file mode 100644 index 00000000..de6ea1fe --- /dev/null +++ b/demos/firebase-nodejs-todolist/pubspec.yaml @@ -0,0 +1,30 @@ +name: firebase_nodejs_todolist +description: Firebase NodeJS Todo List Demo +publish_to: "none" + +version: 1.0.0+1 + +environment: + sdk: ">=3.0.0" + +dependencies: + flutter: + sdk: flutter + + powersync: ^1.16.1 + path_provider: ^2.1.1 + supabase_flutter: ^2.0.1 + path: ^1.8.3 + logging: ^1.1.1 + firebase_core: ^3.8.0 + firebase_auth: ^5.3.3 + http: ^1.2.2 + +dev_dependencies: + flutter_test: + sdk: flutter + + flutter_lints: ^3.0.1 + +flutter: + uses-material-design: true diff --git a/demos/firebase-nodejs-todolist/windows/.gitignore b/demos/firebase-nodejs-todolist/windows/.gitignore new file mode 100644 index 00000000..d492d0d9 --- /dev/null +++ b/demos/firebase-nodejs-todolist/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/demos/firebase-nodejs-todolist/windows/CMakeLists.txt b/demos/firebase-nodejs-todolist/windows/CMakeLists.txt new file mode 100644 index 00000000..ccfc4498 --- /dev/null +++ b/demos/firebase-nodejs-todolist/windows/CMakeLists.txt @@ -0,0 +1,101 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(powersync_flutter_demo LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "powersync_flutter_demo") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/demos/firebase-nodejs-todolist/windows/flutter/CMakeLists.txt b/demos/firebase-nodejs-todolist/windows/flutter/CMakeLists.txt new file mode 100644 index 00000000..930d2071 --- /dev/null +++ b/demos/firebase-nodejs-todolist/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/demos/firebase-nodejs-todolist/windows/flutter/generated_plugin_registrant.cc b/demos/firebase-nodejs-todolist/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..ecfe9c8a --- /dev/null +++ b/demos/firebase-nodejs-todolist/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,29 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + AppLinksPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksPluginCApi")); + FirebaseAuthPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); + FirebaseCorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + PowersyncFlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PowersyncFlutterLibsPlugin")); + Sqlite3FlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/demos/firebase-nodejs-todolist/windows/flutter/generated_plugin_registrant.h b/demos/firebase-nodejs-todolist/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..dc139d85 --- /dev/null +++ b/demos/firebase-nodejs-todolist/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/demos/firebase-nodejs-todolist/windows/flutter/generated_plugins.cmake b/demos/firebase-nodejs-todolist/windows/flutter/generated_plugins.cmake new file mode 100644 index 00000000..4957d4ca --- /dev/null +++ b/demos/firebase-nodejs-todolist/windows/flutter/generated_plugins.cmake @@ -0,0 +1,29 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + app_links + firebase_auth + firebase_core + powersync_flutter_libs + sqlite3_flutter_libs + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/demos/firebase-nodejs-todolist/windows/runner/CMakeLists.txt b/demos/firebase-nodejs-todolist/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..394917c0 --- /dev/null +++ b/demos/firebase-nodejs-todolist/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/demos/firebase-nodejs-todolist/windows/runner/Runner.rc b/demos/firebase-nodejs-todolist/windows/runner/Runner.rc new file mode 100644 index 00000000..75674c07 --- /dev/null +++ b/demos/firebase-nodejs-todolist/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "Journey Mobile Inc" "\0" + VALUE "FileDescription", "powersync_todolist_demo" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "powersync_flutter_demo" "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 Journey Mobile, Inc. All rights reserved." "\0" + VALUE "OriginalFilename", "powersync_todolist_demo.exe" "\0" + VALUE "ProductName", "powersync_todolist_demo" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/demos/firebase-nodejs-todolist/windows/runner/flutter_window.cpp b/demos/firebase-nodejs-todolist/windows/runner/flutter_window.cpp new file mode 100644 index 00000000..b25e363e --- /dev/null +++ b/demos/firebase-nodejs-todolist/windows/runner/flutter_window.cpp @@ -0,0 +1,66 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/demos/firebase-nodejs-todolist/windows/runner/flutter_window.h b/demos/firebase-nodejs-todolist/windows/runner/flutter_window.h new file mode 100644 index 00000000..6da0652f --- /dev/null +++ b/demos/firebase-nodejs-todolist/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/demos/firebase-nodejs-todolist/windows/runner/main.cpp b/demos/firebase-nodejs-todolist/windows/runner/main.cpp new file mode 100644 index 00000000..3eee96b7 --- /dev/null +++ b/demos/firebase-nodejs-todolist/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"powersync_flutter_demo", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/demos/firebase-nodejs-todolist/windows/runner/resource.h b/demos/firebase-nodejs-todolist/windows/runner/resource.h new file mode 100644 index 00000000..66a65d1e --- /dev/null +++ b/demos/firebase-nodejs-todolist/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/demos/firebase-nodejs-todolist/windows/runner/resources/app_icon.ico b/demos/firebase-nodejs-todolist/windows/runner/resources/app_icon.ico new file mode 100644 index 00000000..c04e20ca Binary files /dev/null and b/demos/firebase-nodejs-todolist/windows/runner/resources/app_icon.ico differ diff --git a/demos/firebase-nodejs-todolist/windows/runner/runner.exe.manifest b/demos/firebase-nodejs-todolist/windows/runner/runner.exe.manifest new file mode 100644 index 00000000..a42ea768 --- /dev/null +++ b/demos/firebase-nodejs-todolist/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/demos/firebase-nodejs-todolist/windows/runner/utils.cpp b/demos/firebase-nodejs-todolist/windows/runner/utils.cpp new file mode 100644 index 00000000..f5bf9fa0 --- /dev/null +++ b/demos/firebase-nodejs-todolist/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/demos/firebase-nodejs-todolist/windows/runner/utils.h b/demos/firebase-nodejs-todolist/windows/runner/utils.h new file mode 100644 index 00000000..3879d547 --- /dev/null +++ b/demos/firebase-nodejs-todolist/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/demos/firebase-nodejs-todolist/windows/runner/win32_window.cpp b/demos/firebase-nodejs-todolist/windows/runner/win32_window.cpp new file mode 100644 index 00000000..041a3855 --- /dev/null +++ b/demos/firebase-nodejs-todolist/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/demos/firebase-nodejs-todolist/windows/runner/win32_window.h b/demos/firebase-nodejs-todolist/windows/runner/win32_window.h new file mode 100644 index 00000000..c86632d8 --- /dev/null +++ b/demos/firebase-nodejs-todolist/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/demos/supabase-anonymous-auth/.gitignore b/demos/supabase-anonymous-auth/.gitignore index 0b04140a..777f9a68 100644 --- a/demos/supabase-anonymous-auth/.gitignore +++ b/demos/supabase-anonymous-auth/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related diff --git a/demos/supabase-anonymous-auth/ios/Flutter/AppFrameworkInfo.plist b/demos/supabase-anonymous-auth/ios/Flutter/AppFrameworkInfo.plist index 9625e105..7c569640 100644 --- a/demos/supabase-anonymous-auth/ios/Flutter/AppFrameworkInfo.plist +++ b/demos/supabase-anonymous-auth/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/demos/supabase-anonymous-auth/ios/Podfile b/demos/supabase-anonymous-auth/ios/Podfile index bab7dec0..2c1e086a 100644 --- a/demos/supabase-anonymous-auth/ios/Podfile +++ b/demos/supabase-anonymous-auth/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '11.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -45,4 +45,4 @@ post_install do |installer| end end end -end \ No newline at end of file +end diff --git a/demos/supabase-anonymous-auth/ios/Podfile.lock b/demos/supabase-anonymous-auth/ios/Podfile.lock index 73885df0..77f7310a 100644 --- a/demos/supabase-anonymous-auth/ios/Podfile.lock +++ b/demos/supabase-anonymous-auth/ios/Podfile.lock @@ -1,28 +1,37 @@ PODS: - - app_links (0.0.1): - - Flutter - - camera_avfoundation (0.0.1): + - app_links (0.0.2): - Flutter - Flutter (1.0.0) - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - powersync-sqlite-core (0.4.5) + - powersync_flutter_libs (0.0.1): + - Flutter + - powersync-sqlite-core (~> 0.4.5) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - sqlite3 (3.44.0): - - sqlite3/common (= 3.44.0) - - sqlite3/common (3.44.0) - - sqlite3/fts5 (3.44.0): + - sqlite3 (3.49.2): + - sqlite3/common (= 3.49.2) + - sqlite3/common (3.49.2) + - sqlite3/dbstatvtab (3.49.2): + - sqlite3/common + - sqlite3/fts5 (3.49.2): - sqlite3/common - - sqlite3/perf-threadsafe (3.44.0): + - sqlite3/math (3.49.2): - sqlite3/common - - sqlite3/rtree (3.44.0): + - sqlite3/perf-threadsafe (3.49.2): + - sqlite3/common + - sqlite3/rtree (3.49.2): - sqlite3/common - sqlite3_flutter_libs (0.0.1): - Flutter - - sqlite3 (~> 3.44.0) + - FlutterMacOS + - sqlite3 (~> 3.49.1) + - sqlite3/dbstatvtab - sqlite3/fts5 + - sqlite3/math - sqlite3/perf-threadsafe - sqlite3/rtree - url_launcher_ios (0.0.1): @@ -30,43 +39,45 @@ PODS: DEPENDENCIES: - app_links (from `.symlinks/plugins/app_links/ios`) - - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) - Flutter (from `Flutter`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - powersync_flutter_libs (from `.symlinks/plugins/powersync_flutter_libs/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: trunk: + - powersync-sqlite-core - sqlite3 EXTERNAL SOURCES: app_links: :path: ".symlinks/plugins/app_links/ios" - camera_avfoundation: - :path: ".symlinks/plugins/camera_avfoundation/ios" Flutter: :path: Flutter path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" + powersync_flutter_libs: + :path: ".symlinks/plugins/powersync_flutter_libs/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqlite3_flutter_libs: - :path: ".symlinks/plugins/sqlite3_flutter_libs/ios" + :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: - app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875 - camera_avfoundation: 3125e8cd1a4387f6f31c6c63abb8a55892a9eeeb - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 - sqlite3: 6e2d4a4879854d0ec86b476bf3c3e30870bac273 - sqlite3_flutter_libs: eb769059df0356dc52ddda040f09cacc9391a7cf - url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b + app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + powersync-sqlite-core: 6f32860379009d2a37cadc9e9427a431bdbd83c8 + powersync_flutter_libs: 7684a62208907328906eb932f1fc8b3d8879974e + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 + sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d -PODFILE CHECKSUM: adb9c9fb405cdaf8ac7cbd45ad6db78acaa58c33 +PODFILE CHECKSUM: 2c1730c97ea13f1ea48b32e9c79de785b4f2f02f -COCOAPODS: 1.14.3 +COCOAPODS: 1.16.2 diff --git a/demos/supabase-anonymous-auth/ios/Runner.xcodeproj/project.pbxproj b/demos/supabase-anonymous-auth/ios/Runner.xcodeproj/project.pbxproj index 6a93f233..16636b7a 100644 --- a/demos/supabase-anonymous-auth/ios/Runner.xcodeproj/project.pbxproj +++ b/demos/supabase-anonymous-auth/ios/Runner.xcodeproj/project.pbxproj @@ -155,7 +155,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -342,7 +342,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -419,7 +419,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -468,7 +468,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/demos/supabase-anonymous-auth/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demos/supabase-anonymous-auth/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a6b826db..c53e2b31 100644 --- a/demos/supabase-anonymous-auth/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/demos/supabase-anonymous-auth/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ diff --git a/demos/supabase-anonymous-auth/ios/Runner/AppDelegate.swift b/demos/supabase-anonymous-auth/ios/Runner/AppDelegate.swift index 70693e4a..b6363034 100644 --- a/demos/supabase-anonymous-auth/ios/Runner/AppDelegate.swift +++ b/demos/supabase-anonymous-auth/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/demos/supabase-anonymous-auth/macos/Podfile b/demos/supabase-anonymous-auth/macos/Podfile index c795730d..b52666a1 100644 --- a/demos/supabase-anonymous-auth/macos/Podfile +++ b/demos/supabase-anonymous-auth/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.14' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/demos/supabase-anonymous-auth/macos/Podfile.lock b/demos/supabase-anonymous-auth/macos/Podfile.lock index f54327c0..6983b2da 100644 --- a/demos/supabase-anonymous-auth/macos/Podfile.lock +++ b/demos/supabase-anonymous-auth/macos/Podfile.lock @@ -5,24 +5,33 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - powersync-sqlite-core (0.4.5) + - powersync_flutter_libs (0.0.1): + - FlutterMacOS + - powersync-sqlite-core (~> 0.4.5) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - sign_in_with_apple (0.0.1): - - FlutterMacOS - - sqlite3 (3.43.1): - - sqlite3/common (= 3.43.1) - - sqlite3/common (3.43.1) - - sqlite3/fts5 (3.43.1): + - sqlite3 (3.49.2): + - sqlite3/common (= 3.49.2) + - sqlite3/common (3.49.2) + - sqlite3/dbstatvtab (3.49.2): + - sqlite3/common + - sqlite3/fts5 (3.49.2): - sqlite3/common - - sqlite3/perf-threadsafe (3.43.1): + - sqlite3/math (3.49.2): - sqlite3/common - - sqlite3/rtree (3.43.1): + - sqlite3/perf-threadsafe (3.49.2): + - sqlite3/common + - sqlite3/rtree (3.49.2): - sqlite3/common - sqlite3_flutter_libs (0.0.1): + - Flutter - FlutterMacOS - - sqlite3 (~> 3.43.1) + - sqlite3 (~> 3.49.1) + - sqlite3/dbstatvtab - sqlite3/fts5 + - sqlite3/math - sqlite3/perf-threadsafe - sqlite3/rtree - url_launcher_macos (0.0.1): @@ -32,13 +41,14 @@ DEPENDENCIES: - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - powersync_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/powersync_flutter_libs/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - - sign_in_with_apple (from `Flutter/ephemeral/.symlinks/plugins/sign_in_with_apple/macos`) - - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`) + - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) SPEC REPOS: trunk: + - powersync-sqlite-core - sqlite3 EXTERNAL SOURCES: @@ -48,25 +58,26 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral path_provider_foundation: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + powersync_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/powersync_flutter_libs/macos shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin - sign_in_with_apple: - :path: Flutter/ephemeral/.symlinks/plugins/sign_in_with_apple/macos sqlite3_flutter_libs: - :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos + :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: - app_links: 4481ed4d71f384b0c3ae5016f4633aa73d32ff67 - FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 - sign_in_with_apple: a9e97e744e8edc36aefc2723111f652102a7a727 - sqlite3: e0a0623a33a20a47cb5921552aebc6e9e437dc91 - sqlite3_flutter_libs: a91655e4a75a499364f693041aa1c6d1b36b66d0 - url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 + app_links: afe860c55c7ef176cea7fb630a2b7d7736de591d + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + powersync-sqlite-core: 6f32860379009d2a37cadc9e9427a431bdbd83c8 + powersync_flutter_libs: 41d8a7b193abf15e46f95f0ec1229d86b6893171 + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 + sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 -PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 +PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 -COCOAPODS: 1.12.1 +COCOAPODS: 1.16.2 diff --git a/demos/supabase-anonymous-auth/pubspec.lock b/demos/supabase-anonymous-auth/pubspec.lock index 452c0ea1..48c25b82 100644 --- a/demos/supabase-anonymous-auth/pubspec.lock +++ b/demos/supabase-anonymous-auth/pubspec.lock @@ -5,42 +5,66 @@ packages: dependency: transitive description: name: app_links - sha256: "3ced568a5d9e309e99af71285666f1f3117bddd0bd5b3317979dccc1a40cada4" + sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba" url: "https://pub.dev" source: hosted - version: "3.5.1" + version: "6.4.0" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" args: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.7.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -53,74 +77,58 @@ packages: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" - fetch_api: - dependency: transitive - description: - name: fetch_api - sha256: "97f46c25b480aad74f7cc2ad7ccba2c5c6f08d008e68f95c1077286ce243d0e6" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - fetch_client: - dependency: transitive - description: - name: fetch_client - sha256: "9666ee14536778474072245ed5cba07db81ae8eb5de3b7bf4a2d1e2c49696092" - url: "https://pub.dev" - source: hosted - version: "1.1.2" + version: "1.3.3" ffi: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -148,18 +156,18 @@ packages: dependency: transitive description: name: functions_client - sha256: "9a0ab83a525c8691a6724746e642de755a299afa04158807787364cd9e718001" + sha256: b410e4d609522357396cd84bb9a8f6e3a4561b5f7d3ce82267f6f1c2af42f16b url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.4.2" gotrue: dependency: transitive description: name: gotrue - sha256: a0eee21a7e8ec09e6bbd5c9a36e31e423827b575ba6fc2dd049805dcfaac5b02 + sha256: "04a6efacffd42773ed96dc752f19bb20a1fbc383e81ba82659072b775cf62912" url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.12.0" gtk: dependency: transitive description: @@ -172,26 +180,18 @@ packages: dependency: transitive description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.4.0" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" - url: "https://pub.dev" - source: hosted - version: "4.0.2" - js: - dependency: transitive - description: - name: js - sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "4.1.2" json_annotation: dependency: transitive description: @@ -212,18 +212,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -244,18 +244,18 @@ packages: dependency: "direct main" description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -268,18 +268,18 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mime: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "2.0.0" mutex: dependency: transitive description: @@ -292,34 +292,34 @@ packages: dependency: "direct main" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_provider: dependency: "direct main" description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "51f0d2c554cfbc9d6a312ab35152fc77e2f0b758ce9f1a444a3a1e5b8f3c6b7f" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.2.17" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -340,18 +340,18 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" platform: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -364,48 +364,55 @@ packages: dependency: transitive description: name: postgrest - sha256: "9a3b590cf123f8d323b6a918702e037f037027d12a01902f9dc6ee38fdc05d6c" + sha256: "10b81a23b1c829ccadf68c626b4d66666453a1474d24c563f313f5ca7851d575" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.4.2" powersync: dependency: "direct main" description: path: "../../packages/powersync" relative: true source: path - version: "1.9.2" + version: "1.15.0" + powersync_core: + dependency: "direct overridden" + description: + path: "../../packages/powersync_core" + relative: true + source: path + version: "1.5.0" powersync_flutter_libs: dependency: "direct overridden" description: path: "../../packages/powersync_flutter_libs" relative: true source: path - version: "0.4.2" + version: "0.4.10" pub_semver: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" realtime_client: dependency: transitive description: name: realtime_client - sha256: bb6747fe2feff7f8349d563ac9d4a8f10ac2dd809bdff1e7e319321d5ea16b49 + sha256: "3a0a99b5bd0fc3b35e8ee846d9a22fa2c2117f7ef1cb73d1e5f08f6c3d09c4e9" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.5.0" retry: dependency: transitive description: @@ -418,79 +425,79 @@ packages: dependency: transitive description: name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" url: "https://pub.dev" source: hosted - version: "0.27.7" + version: "0.28.0" shared_preferences: dependency: transitive description: name: shared_preferences - sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.10" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.5.4" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.3" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" sprintf: dependency: transitive description: @@ -503,106 +510,106 @@ packages: dependency: transitive description: name: sqlite3 - sha256: bb174b3ec2527f9c5f680f73a89af8149dd99782fbb56ea88ad0807c5638f2ed + sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e" url: "https://pub.dev" source: hosted - version: "2.4.7" + version: "2.7.5" sqlite3_flutter_libs: dependency: transitive description: name: sqlite3_flutter_libs - sha256: "62bbb4073edbcdf53f40c80775f33eea01d301b7b81417e5b3fb7395416258c1" + sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14" url: "https://pub.dev" source: hosted - version: "0.5.24" + version: "0.5.32" sqlite3_web: dependency: transitive description: name: sqlite3_web - sha256: f22d1dda7a40be0867984f55cdf5c2d599e5f05d3be4a642d78f38b38983f554 + sha256: "967e076442f7e1233bd7241ca61f3efe4c7fc168dac0f38411bdb3bdf471eb3c" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.3.1" sqlite_async: dependency: "direct main" description: name: sqlite_async - sha256: d66fb6e6d07c1a834743326c033029f75becbb1fad6823d709f921872abc3d5b + sha256: a60e8d5c8df8e694933bd5a312c38393e79ad77d784bb91c6f38ba627bfb7aec url: "https://pub.dev" source: hosted - version: "0.11.0" + version: "0.11.4" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" storage_client: dependency: transitive description: name: storage_client - sha256: bf5589d5de61a2451edb1b8960a0e673d4bb5c42ecc4dddf7c051a93789ced34 + sha256: "09bac4d75eea58e8113ca928e6655a09cc8059e6d1b472ee801f01fde815bcfc" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.4.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" supabase: dependency: transitive description: name: supabase - sha256: "2ddedf13f6dc013084569673dff7a7d540f5eacdd5b36fede8d58322e5d79c55" + sha256: f00172f5f0b2148ea1c573f52862d50cacb6f353f579f741fa35e51704845958 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.7.0" supabase_flutter: dependency: "direct main" description: name: supabase_flutter - sha256: "2d9683a15098258de137cb9182e695fa2a1a0f366c7409c2a6e6d47bc5a42be3" + sha256: d88eccf9e46e57129725a08e72a3109b6f780921fdc27fe3d7669a11ae80906b url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.9.0" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.4" typed_data: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" universal_io: dependency: "direct main" description: @@ -615,42 +622,42 @@ packages: dependency: transitive description: name: url_launcher - sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e" + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.2.5" + version: "6.3.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.16" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5" + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" url: "https://pub.dev" source: hosted - version: "6.2.5" + version: "6.3.3" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: @@ -663,26 +670,26 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.1" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.4" uuid: dependency: transitive description: name: uuid - sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.5.1" vector_math: dependency: transitive description: @@ -695,58 +702,58 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "15.0.0" web: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" - web_socket_channel: + version: "1.1.1" + web_socket: dependency: transitive description: - name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "2.4.0" - win32: + version: "1.0.1" + web_socket_channel: dependency: transitive description: - name: win32 - sha256: "0a989dc7ca2bb51eac91e8fd00851297cfffd641aa7538b165c62637ca0eaa4a" + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "3.0.3" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" yaml: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" yet_another_json_isolate: dependency: transitive description: name: yet_another_json_isolate - sha256: e727502a2640d65b4b8a8a6cb48af9dd0cbe644ba4b3ee667c7f4afa0c1d6069 + sha256: fe45897501fa156ccefbfb9359c9462ce5dec092f05e8a56109db30be864f01e url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.19.0" + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/demos/supabase-anonymous-auth/pubspec.yaml b/demos/supabase-anonymous-auth/pubspec.yaml index 2e7b1a14..b4f44bcb 100644 --- a/demos/supabase-anonymous-auth/pubspec.yaml +++ b/demos/supabase-anonymous-auth/pubspec.yaml @@ -11,12 +11,12 @@ dependencies: flutter: sdk: flutter - powersync: ^1.9.3 + powersync: ^1.16.1 path_provider: ^2.1.1 supabase_flutter: ^2.0.2 path: ^1.8.3 logging: ^1.2.0 - sqlite_async: ^0.11.0 + sqlite_async: ^0.12.0 universal_io: ^2.2.2 dev_dependencies: diff --git a/demos/supabase-edge-function-auth/.gitignore b/demos/supabase-edge-function-auth/.gitignore index 0b04140a..777f9a68 100644 --- a/demos/supabase-edge-function-auth/.gitignore +++ b/demos/supabase-edge-function-auth/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related diff --git a/demos/supabase-edge-function-auth/ios/Flutter/AppFrameworkInfo.plist b/demos/supabase-edge-function-auth/ios/Flutter/AppFrameworkInfo.plist index 9625e105..7c569640 100644 --- a/demos/supabase-edge-function-auth/ios/Flutter/AppFrameworkInfo.plist +++ b/demos/supabase-edge-function-auth/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/demos/supabase-edge-function-auth/ios/Podfile b/demos/supabase-edge-function-auth/ios/Podfile index bab7dec0..2c1e086a 100644 --- a/demos/supabase-edge-function-auth/ios/Podfile +++ b/demos/supabase-edge-function-auth/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '11.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -45,4 +45,4 @@ post_install do |installer| end end end -end \ No newline at end of file +end diff --git a/demos/supabase-edge-function-auth/ios/Podfile.lock b/demos/supabase-edge-function-auth/ios/Podfile.lock index 73885df0..77f7310a 100644 --- a/demos/supabase-edge-function-auth/ios/Podfile.lock +++ b/demos/supabase-edge-function-auth/ios/Podfile.lock @@ -1,28 +1,37 @@ PODS: - - app_links (0.0.1): - - Flutter - - camera_avfoundation (0.0.1): + - app_links (0.0.2): - Flutter - Flutter (1.0.0) - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - powersync-sqlite-core (0.4.5) + - powersync_flutter_libs (0.0.1): + - Flutter + - powersync-sqlite-core (~> 0.4.5) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - sqlite3 (3.44.0): - - sqlite3/common (= 3.44.0) - - sqlite3/common (3.44.0) - - sqlite3/fts5 (3.44.0): + - sqlite3 (3.49.2): + - sqlite3/common (= 3.49.2) + - sqlite3/common (3.49.2) + - sqlite3/dbstatvtab (3.49.2): + - sqlite3/common + - sqlite3/fts5 (3.49.2): - sqlite3/common - - sqlite3/perf-threadsafe (3.44.0): + - sqlite3/math (3.49.2): - sqlite3/common - - sqlite3/rtree (3.44.0): + - sqlite3/perf-threadsafe (3.49.2): + - sqlite3/common + - sqlite3/rtree (3.49.2): - sqlite3/common - sqlite3_flutter_libs (0.0.1): - Flutter - - sqlite3 (~> 3.44.0) + - FlutterMacOS + - sqlite3 (~> 3.49.1) + - sqlite3/dbstatvtab - sqlite3/fts5 + - sqlite3/math - sqlite3/perf-threadsafe - sqlite3/rtree - url_launcher_ios (0.0.1): @@ -30,43 +39,45 @@ PODS: DEPENDENCIES: - app_links (from `.symlinks/plugins/app_links/ios`) - - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) - Flutter (from `Flutter`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - powersync_flutter_libs (from `.symlinks/plugins/powersync_flutter_libs/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: trunk: + - powersync-sqlite-core - sqlite3 EXTERNAL SOURCES: app_links: :path: ".symlinks/plugins/app_links/ios" - camera_avfoundation: - :path: ".symlinks/plugins/camera_avfoundation/ios" Flutter: :path: Flutter path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" + powersync_flutter_libs: + :path: ".symlinks/plugins/powersync_flutter_libs/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqlite3_flutter_libs: - :path: ".symlinks/plugins/sqlite3_flutter_libs/ios" + :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: - app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875 - camera_avfoundation: 3125e8cd1a4387f6f31c6c63abb8a55892a9eeeb - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 - sqlite3: 6e2d4a4879854d0ec86b476bf3c3e30870bac273 - sqlite3_flutter_libs: eb769059df0356dc52ddda040f09cacc9391a7cf - url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b + app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + powersync-sqlite-core: 6f32860379009d2a37cadc9e9427a431bdbd83c8 + powersync_flutter_libs: 7684a62208907328906eb932f1fc8b3d8879974e + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 + sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d -PODFILE CHECKSUM: adb9c9fb405cdaf8ac7cbd45ad6db78acaa58c33 +PODFILE CHECKSUM: 2c1730c97ea13f1ea48b32e9c79de785b4f2f02f -COCOAPODS: 1.14.3 +COCOAPODS: 1.16.2 diff --git a/demos/supabase-edge-function-auth/ios/Runner.xcodeproj/project.pbxproj b/demos/supabase-edge-function-auth/ios/Runner.xcodeproj/project.pbxproj index 6a93f233..16636b7a 100644 --- a/demos/supabase-edge-function-auth/ios/Runner.xcodeproj/project.pbxproj +++ b/demos/supabase-edge-function-auth/ios/Runner.xcodeproj/project.pbxproj @@ -155,7 +155,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -342,7 +342,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -419,7 +419,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -468,7 +468,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/demos/supabase-edge-function-auth/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demos/supabase-edge-function-auth/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a6b826db..c53e2b31 100644 --- a/demos/supabase-edge-function-auth/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/demos/supabase-edge-function-auth/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ diff --git a/demos/supabase-edge-function-auth/ios/Runner/AppDelegate.swift b/demos/supabase-edge-function-auth/ios/Runner/AppDelegate.swift index 70693e4a..b6363034 100644 --- a/demos/supabase-edge-function-auth/ios/Runner/AppDelegate.swift +++ b/demos/supabase-edge-function-auth/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/demos/supabase-edge-function-auth/macos/Podfile b/demos/supabase-edge-function-auth/macos/Podfile index c795730d..b52666a1 100644 --- a/demos/supabase-edge-function-auth/macos/Podfile +++ b/demos/supabase-edge-function-auth/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.14' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/demos/supabase-edge-function-auth/macos/Podfile.lock b/demos/supabase-edge-function-auth/macos/Podfile.lock index f54327c0..6983b2da 100644 --- a/demos/supabase-edge-function-auth/macos/Podfile.lock +++ b/demos/supabase-edge-function-auth/macos/Podfile.lock @@ -5,24 +5,33 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - powersync-sqlite-core (0.4.5) + - powersync_flutter_libs (0.0.1): + - FlutterMacOS + - powersync-sqlite-core (~> 0.4.5) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - sign_in_with_apple (0.0.1): - - FlutterMacOS - - sqlite3 (3.43.1): - - sqlite3/common (= 3.43.1) - - sqlite3/common (3.43.1) - - sqlite3/fts5 (3.43.1): + - sqlite3 (3.49.2): + - sqlite3/common (= 3.49.2) + - sqlite3/common (3.49.2) + - sqlite3/dbstatvtab (3.49.2): + - sqlite3/common + - sqlite3/fts5 (3.49.2): - sqlite3/common - - sqlite3/perf-threadsafe (3.43.1): + - sqlite3/math (3.49.2): - sqlite3/common - - sqlite3/rtree (3.43.1): + - sqlite3/perf-threadsafe (3.49.2): + - sqlite3/common + - sqlite3/rtree (3.49.2): - sqlite3/common - sqlite3_flutter_libs (0.0.1): + - Flutter - FlutterMacOS - - sqlite3 (~> 3.43.1) + - sqlite3 (~> 3.49.1) + - sqlite3/dbstatvtab - sqlite3/fts5 + - sqlite3/math - sqlite3/perf-threadsafe - sqlite3/rtree - url_launcher_macos (0.0.1): @@ -32,13 +41,14 @@ DEPENDENCIES: - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - powersync_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/powersync_flutter_libs/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - - sign_in_with_apple (from `Flutter/ephemeral/.symlinks/plugins/sign_in_with_apple/macos`) - - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`) + - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) SPEC REPOS: trunk: + - powersync-sqlite-core - sqlite3 EXTERNAL SOURCES: @@ -48,25 +58,26 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral path_provider_foundation: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + powersync_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/powersync_flutter_libs/macos shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin - sign_in_with_apple: - :path: Flutter/ephemeral/.symlinks/plugins/sign_in_with_apple/macos sqlite3_flutter_libs: - :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos + :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: - app_links: 4481ed4d71f384b0c3ae5016f4633aa73d32ff67 - FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 - sign_in_with_apple: a9e97e744e8edc36aefc2723111f652102a7a727 - sqlite3: e0a0623a33a20a47cb5921552aebc6e9e437dc91 - sqlite3_flutter_libs: a91655e4a75a499364f693041aa1c6d1b36b66d0 - url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 + app_links: afe860c55c7ef176cea7fb630a2b7d7736de591d + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + powersync-sqlite-core: 6f32860379009d2a37cadc9e9427a431bdbd83c8 + powersync_flutter_libs: 41d8a7b193abf15e46f95f0ec1229d86b6893171 + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 + sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 -PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 +PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 -COCOAPODS: 1.12.1 +COCOAPODS: 1.16.2 diff --git a/demos/supabase-edge-function-auth/pubspec.lock b/demos/supabase-edge-function-auth/pubspec.lock index 08973ab4..48c25b82 100644 --- a/demos/supabase-edge-function-auth/pubspec.lock +++ b/demos/supabase-edge-function-auth/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: app_links - sha256: f04c3ca96426baba784c736a201926bd4145524c36a1b38942a351b033305e21 + sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba" url: "https://pub.dev" source: hosted - version: "6.2.1" + version: "6.4.0" app_links_linux: dependency: transitive description: @@ -37,34 +37,34 @@ packages: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.7.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -77,74 +77,58 @@ packages: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" crypto: dependency: transitive description: name: crypto - sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.6" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.dev" - source: hosted - version: "1.3.1" - fetch_api: - dependency: transitive - description: - name: fetch_api - sha256: "97f46c25b480aad74f7cc2ad7ccba2c5c6f08d008e68f95c1077286ce243d0e6" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "2.2.0" - fetch_client: - dependency: transitive - description: - name: fetch_client - sha256: "9666ee14536778474072245ed5cba07db81ae8eb5de3b7bf4a2d1e2c49696092" - url: "https://pub.dev" - source: hosted - version: "1.1.2" + version: "1.3.3" ffi: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -172,18 +156,18 @@ packages: dependency: transitive description: name: functions_client - sha256: e63f49cd3b41727f47b3bde284a11a4ac62839e0604f64077d4257487510e484 + sha256: b410e4d609522357396cd84bb9a8f6e3a4561b5f7d3ce82267f6f1c2af42f16b url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.2" gotrue: dependency: transitive description: name: gotrue - sha256: "8703db795511f69194fe77125a0c838bbb6befc2f95717b6e40331784a8bdecb" + sha256: "04a6efacffd42773ed96dc752f19bb20a1fbc383e81ba82659072b775cf62912" url: "https://pub.dev" source: hosted - version: "2.8.4" + version: "2.12.0" gtk: dependency: transitive description: @@ -196,26 +180,18 @@ packages: dependency: transitive description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.4.0" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" - url: "https://pub.dev" - source: hosted - version: "4.0.2" - js: - dependency: transitive - description: - name: js - sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "4.1.2" json_annotation: dependency: transitive description: @@ -236,18 +212,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -268,18 +244,18 @@ packages: dependency: "direct main" description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -292,18 +268,18 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mime: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "2.0.0" mutex: dependency: transitive description: @@ -316,34 +292,34 @@ packages: dependency: "direct main" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_provider: dependency: "direct main" description: name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.10" + version: "2.2.17" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -372,10 +348,10 @@ packages: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -388,48 +364,55 @@ packages: dependency: transitive description: name: postgrest - sha256: c4197238601c7c3103b03a4bb77f2050b17d0064bf8b968309421abdebbb7f0e + sha256: "10b81a23b1c829ccadf68c626b4d66666453a1474d24c563f313f5ca7851d575" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.4.2" powersync: dependency: "direct main" description: path: "../../packages/powersync" relative: true source: path - version: "1.9.2" + version: "1.15.0" + powersync_core: + dependency: "direct overridden" + description: + path: "../../packages/powersync_core" + relative: true + source: path + version: "1.5.0" powersync_flutter_libs: dependency: "direct overridden" description: path: "../../packages/powersync_flutter_libs" relative: true source: path - version: "0.4.2" + version: "0.4.10" pub_semver: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" realtime_client: dependency: transitive description: name: realtime_client - sha256: d897a65ee3b1b5ddc1cf606f0b83792262d38fd5679c2df7e38da29c977513da + sha256: "3a0a99b5bd0fc3b35e8ee846d9a22fa2c2117f7ef1cb73d1e5f08f6c3d09c4e9" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.5.0" retry: dependency: transitive description: @@ -450,26 +433,26 @@ packages: dependency: transitive description: name: shared_preferences - sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: a7e8467e9181cef109f601e3f65765685786c1a738a83d7fbbde377589c0d974 + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.10" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.4" shared_preferences_linux: dependency: transitive description: @@ -490,10 +473,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" shared_preferences_windows: dependency: transitive description: @@ -506,15 +489,15 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" sprintf: dependency: transitive description: @@ -527,106 +510,106 @@ packages: dependency: transitive description: name: sqlite3 - sha256: bb174b3ec2527f9c5f680f73a89af8149dd99782fbb56ea88ad0807c5638f2ed + sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e" url: "https://pub.dev" source: hosted - version: "2.4.7" + version: "2.7.5" sqlite3_flutter_libs: dependency: transitive description: name: sqlite3_flutter_libs - sha256: "62bbb4073edbcdf53f40c80775f33eea01d301b7b81417e5b3fb7395416258c1" + sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14" url: "https://pub.dev" source: hosted - version: "0.5.24" + version: "0.5.32" sqlite3_web: dependency: transitive description: name: sqlite3_web - sha256: f22d1dda7a40be0867984f55cdf5c2d599e5f05d3be4a642d78f38b38983f554 + sha256: "967e076442f7e1233bd7241ca61f3efe4c7fc168dac0f38411bdb3bdf471eb3c" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.3.1" sqlite_async: dependency: "direct main" description: name: sqlite_async - sha256: d66fb6e6d07c1a834743326c033029f75becbb1fad6823d709f921872abc3d5b + sha256: a60e8d5c8df8e694933bd5a312c38393e79ad77d784bb91c6f38ba627bfb7aec url: "https://pub.dev" source: hosted - version: "0.11.0" + version: "0.11.4" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" storage_client: dependency: transitive description: name: storage_client - sha256: "28c147c805304dbc2b762becd1fc26ee0cb621ace3732b9ae61ef979aab8b367" + sha256: "09bac4d75eea58e8113ca928e6655a09cc8059e6d1b472ee801f01fde815bcfc" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.4.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" supabase: dependency: transitive description: name: supabase - sha256: "4ed1cf3298f39865c05b2d8557f92eb131a9b9af70e32e218672a0afce01a6bc" + sha256: f00172f5f0b2148ea1c573f52862d50cacb6f353f579f741fa35e51704845958 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.7.0" supabase_flutter: dependency: "direct main" description: name: supabase_flutter - sha256: ff6ba3048fd47d831fdc0027d3efb99346d99b95becfcb406562454bd9b229c5 + sha256: d88eccf9e46e57129725a08e72a3109b6f780921fdc27fe3d7669a11ae80906b url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.9.0" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.4" typed_data: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" universal_io: dependency: "direct main" description: @@ -639,42 +622,42 @@ packages: dependency: transitive description: name: url_launcher - sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: f0c73347dfcfa5b3db8bc06e1502668265d39c08f310c29bff4e28eea9699f79 + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" url: "https://pub.dev" source: hosted - version: "6.3.9" + version: "6.3.16" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.3" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: @@ -687,26 +670,26 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.1" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.4" uuid: dependency: transitive description: name: uuid - sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "4.4.2" + version: "4.5.1" vector_math: dependency: transitive description: @@ -719,58 +702,58 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "15.0.0" web: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web_socket: dependency: transitive description: name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" yaml: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" yet_another_json_isolate: dependency: transitive description: name: yet_another_json_isolate - sha256: "47ed3900e6b0e4dfe378811a4402e85b7fc126a7daa94f840fef65ea9c8e46f4" + sha256: fe45897501fa156ccefbfb9359c9462ce5dec092f05e8a56109db30be864f01e url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/demos/supabase-edge-function-auth/pubspec.yaml b/demos/supabase-edge-function-auth/pubspec.yaml index 9a576368..f1f5ddcb 100644 --- a/demos/supabase-edge-function-auth/pubspec.yaml +++ b/demos/supabase-edge-function-auth/pubspec.yaml @@ -11,12 +11,12 @@ dependencies: flutter: sdk: flutter - powersync: ^1.9.3 + powersync: ^1.16.1 path_provider: ^2.1.1 supabase_flutter: ^2.0.2 path: ^1.8.3 logging: ^1.2.0 - sqlite_async: ^0.11.0 + sqlite_async: ^0.12.0 universal_io: ^2.2.2 dev_dependencies: diff --git a/demos/supabase-simple-chat/ios/Flutter/AppFrameworkInfo.plist b/demos/supabase-simple-chat/ios/Flutter/AppFrameworkInfo.plist index 9625e105..7c569640 100644 --- a/demos/supabase-simple-chat/ios/Flutter/AppFrameworkInfo.plist +++ b/demos/supabase-simple-chat/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/demos/supabase-simple-chat/ios/Podfile b/demos/supabase-simple-chat/ios/Podfile index 164df534..3e44f9c6 100644 --- a/demos/supabase-simple-chat/ios/Podfile +++ b/demos/supabase-simple-chat/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '12.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/demos/supabase-simple-chat/ios/Podfile.lock b/demos/supabase-simple-chat/ios/Podfile.lock index 6dbc5f5f..6108d2f1 100644 --- a/demos/supabase-simple-chat/ios/Podfile.lock +++ b/demos/supabase-simple-chat/ios/Podfile.lock @@ -1,47 +1,54 @@ PODS: - - app_links (0.0.1): + - app_links (0.0.2): - Flutter - Flutter (1.0.0) - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - powersync-sqlite-core (0.4.5) + - powersync_flutter_libs (0.0.1): + - Flutter + - powersync-sqlite-core (~> 0.4.5) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - sign_in_with_apple (0.0.1): - - Flutter - - sqlite3 (3.45.1): - - sqlite3/common (= 3.45.1) - - sqlite3/common (3.45.1) - - sqlite3/fts5 (3.45.1): + - sqlite3 (3.49.2): + - sqlite3/common (= 3.49.2) + - sqlite3/common (3.49.2) + - sqlite3/dbstatvtab (3.49.2): + - sqlite3/common + - sqlite3/fts5 (3.49.2): - sqlite3/common - - sqlite3/perf-threadsafe (3.45.1): + - sqlite3/math (3.49.2): - sqlite3/common - - sqlite3/rtree (3.45.1): + - sqlite3/perf-threadsafe (3.49.2): + - sqlite3/common + - sqlite3/rtree (3.49.2): - sqlite3/common - sqlite3_flutter_libs (0.0.1): - Flutter - - sqlite3 (~> 3.45.1) + - FlutterMacOS + - sqlite3 (~> 3.49.1) + - sqlite3/dbstatvtab - sqlite3/fts5 + - sqlite3/math - sqlite3/perf-threadsafe - sqlite3/rtree - url_launcher_ios (0.0.1): - Flutter - - webview_flutter_wkwebview (0.0.1): - - Flutter DEPENDENCIES: - app_links (from `.symlinks/plugins/app_links/ios`) - Flutter (from `Flutter`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - powersync_flutter_libs (from `.symlinks/plugins/powersync_flutter_libs/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`) - - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) SPEC REPOS: trunk: + - powersync-sqlite-core - sqlite3 EXTERNAL SOURCES: @@ -51,28 +58,26 @@ EXTERNAL SOURCES: :path: Flutter path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" + powersync_flutter_libs: + :path: ".symlinks/plugins/powersync_flutter_libs/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" - sign_in_with_apple: - :path: ".symlinks/plugins/sign_in_with_apple/ios" sqlite3_flutter_libs: - :path: ".symlinks/plugins/sqlite3_flutter_libs/ios" + :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" - webview_flutter_wkwebview: - :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" SPEC CHECKSUMS: - app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875 - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c - shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 - sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440 - sqlite3: 73b7fc691fdc43277614250e04d183740cb15078 - sqlite3_flutter_libs: af0e8fe9bce48abddd1ffdbbf839db0302d72d80 - url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 - webview_flutter_wkwebview: be0f0d33777f1bfd0c9fdcb594786704dbf65f36 + app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + powersync-sqlite-core: 6f32860379009d2a37cadc9e9427a431bdbd83c8 + powersync_flutter_libs: 7684a62208907328906eb932f1fc8b3d8879974e + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 + sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d -PODFILE CHECKSUM: 7be2f5f74864d463a8ad433546ed1de7e0f29aef +PODFILE CHECKSUM: a57f30d18f102dd3ce366b1d62a55ecbef2158e5 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/demos/supabase-simple-chat/ios/Runner.xcodeproj/project.pbxproj b/demos/supabase-simple-chat/ios/Runner.xcodeproj/project.pbxproj index 7232e3ea..68a37668 100644 --- a/demos/supabase-simple-chat/ios/Runner.xcodeproj/project.pbxproj +++ b/demos/supabase-simple-chat/ios/Runner.xcodeproj/project.pbxproj @@ -216,7 +216,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C8080294A63A400263BE5 = { @@ -453,7 +453,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -580,7 +580,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -629,7 +629,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/demos/supabase-simple-chat/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demos/supabase-simple-chat/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 87131a09..15cada48 100644 --- a/demos/supabase-simple-chat/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/demos/supabase-simple-chat/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ diff --git a/demos/supabase-simple-chat/ios/Runner/AppDelegate.swift b/demos/supabase-simple-chat/ios/Runner/AppDelegate.swift index 70693e4a..b6363034 100644 --- a/demos/supabase-simple-chat/ios/Runner/AppDelegate.swift +++ b/demos/supabase-simple-chat/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/demos/supabase-simple-chat/macos/Podfile b/demos/supabase-simple-chat/macos/Podfile index c795730d..b52666a1 100644 --- a/demos/supabase-simple-chat/macos/Podfile +++ b/demos/supabase-simple-chat/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.14' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/demos/supabase-simple-chat/macos/Podfile.lock b/demos/supabase-simple-chat/macos/Podfile.lock new file mode 100644 index 00000000..6983b2da --- /dev/null +++ b/demos/supabase-simple-chat/macos/Podfile.lock @@ -0,0 +1,83 @@ +PODS: + - app_links (1.0.0): + - FlutterMacOS + - FlutterMacOS (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - powersync-sqlite-core (0.4.5) + - powersync_flutter_libs (0.0.1): + - FlutterMacOS + - powersync-sqlite-core (~> 0.4.5) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (3.49.2): + - sqlite3/common (= 3.49.2) + - sqlite3/common (3.49.2) + - sqlite3/dbstatvtab (3.49.2): + - sqlite3/common + - sqlite3/fts5 (3.49.2): + - sqlite3/common + - sqlite3/math (3.49.2): + - sqlite3/common + - sqlite3/perf-threadsafe (3.49.2): + - sqlite3/common + - sqlite3/rtree (3.49.2): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (~> 3.49.1) + - sqlite3/dbstatvtab + - sqlite3/fts5 + - sqlite3/math + - sqlite3/perf-threadsafe + - sqlite3/rtree + - url_launcher_macos (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - powersync_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/powersync_flutter_libs/macos`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + +SPEC REPOS: + trunk: + - powersync-sqlite-core + - sqlite3 + +EXTERNAL SOURCES: + app_links: + :path: Flutter/ephemeral/.symlinks/plugins/app_links/macos + FlutterMacOS: + :path: Flutter/ephemeral + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + powersync_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/powersync_flutter_libs/macos + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + sqlite3_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + +SPEC CHECKSUMS: + app_links: afe860c55c7ef176cea7fb630a2b7d7736de591d + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + powersync-sqlite-core: 6f32860379009d2a37cadc9e9427a431bdbd83c8 + powersync_flutter_libs: 41d8a7b193abf15e46f95f0ec1229d86b6893171 + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 + sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 + +PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 + +COCOAPODS: 1.16.2 diff --git a/demos/supabase-simple-chat/macos/Runner.xcodeproj/project.pbxproj b/demos/supabase-simple-chat/macos/Runner.xcodeproj/project.pbxproj index 7fe41dae..1c21cabe 100644 --- a/demos/supabase-simple-chat/macos/Runner.xcodeproj/project.pbxproj +++ b/demos/supabase-simple-chat/macos/Runner.xcodeproj/project.pbxproj @@ -21,6 +21,8 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + 1E4CDBAEBE9154C7F26D0A91 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D9733994F6B92C007FE8C102 /* Pods_Runner.framework */; }; + 2A4895D0BF74E5000FECF75C /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9D1A3062AADC16F6873C7A88 /* Pods_RunnerTests.framework */; }; 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; @@ -64,7 +66,7 @@ 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* supabase_tutorial_chat_app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "supabase_tutorial_chat_app.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* supabase_tutorial_chat_app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = supabase_tutorial_chat_app.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -76,8 +78,16 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 4119129E140BF2A98A74E736 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 414DA522E744E5F8711DCAB0 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 4BABC2241892D6C3DF961EC6 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 4EFA73CF76E64E7CC74AD3BD /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + 99432E67B36414807BF78B2C /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 9B88A251F7A6114EC05D3BA1 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 9D1A3062AADC16F6873C7A88 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D9733994F6B92C007FE8C102 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,6 +95,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 2A4895D0BF74E5000FECF75C /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -92,12 +103,27 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 1E4CDBAEBE9154C7F26D0A91 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0272BFDB59D0F5397E903FC2 /* Pods */ = { + isa = PBXGroup; + children = ( + 4BABC2241892D6C3DF961EC6 /* Pods-Runner.debug.xcconfig */, + 9B88A251F7A6114EC05D3BA1 /* Pods-Runner.release.xcconfig */, + 4EFA73CF76E64E7CC74AD3BD /* Pods-Runner.profile.xcconfig */, + 414DA522E744E5F8711DCAB0 /* Pods-RunnerTests.debug.xcconfig */, + 99432E67B36414807BF78B2C /* Pods-RunnerTests.release.xcconfig */, + 4119129E140BF2A98A74E736 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; 331C80D6294CF71000263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( @@ -125,6 +151,7 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + 0272BFDB59D0F5397E903FC2 /* Pods */, ); sourceTree = ""; }; @@ -175,6 +202,8 @@ D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + D9733994F6B92C007FE8C102 /* Pods_Runner.framework */, + 9D1A3062AADC16F6873C7A88 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -186,6 +215,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + A8DB1B20C2102EDC8F456153 /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -204,11 +234,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 355BF6106EDA763284A00066 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + 5A07EE710C2C2774485C2634 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -328,6 +360,67 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + 355BF6106EDA763284A00066 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 5A07EE710C2C2774485C2634 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + A8DB1B20C2102EDC8F456153 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -379,6 +472,7 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 414DA522E744E5F8711DCAB0 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -393,6 +487,7 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 99432E67B36414807BF78B2C /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -407,6 +502,7 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 4119129E140BF2A98A74E736 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; diff --git a/demos/supabase-simple-chat/macos/Runner.xcworkspace/contents.xcworkspacedata b/demos/supabase-simple-chat/macos/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16..21a3cc14 100644 --- a/demos/supabase-simple-chat/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/demos/supabase-simple-chat/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/demos/supabase-simple-chat/pubspec.lock b/demos/supabase-simple-chat/pubspec.lock index bfc1ed6a..d65ac498 100644 --- a/demos/supabase-simple-chat/pubspec.lock +++ b/demos/supabase-simple-chat/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: app_links - sha256: f04c3ca96426baba784c736a201926bd4145524c36a1b38942a351b033305e21 + sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba" url: "https://pub.dev" source: hosted - version: "6.2.1" + version: "6.4.0" app_links_linux: dependency: transitive description: @@ -37,34 +37,34 @@ packages: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.7.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -77,26 +77,26 @@ packages: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" crypto: dependency: transitive description: name: crypto - sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.6" cupertino_icons: dependency: "direct main" description: @@ -109,50 +109,34 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.dev" - source: hosted - version: "1.3.1" - fetch_api: - dependency: transitive - description: - name: fetch_api - sha256: "97f46c25b480aad74f7cc2ad7ccba2c5c6f08d008e68f95c1077286ce243d0e6" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "2.2.0" - fetch_client: - dependency: transitive - description: - name: fetch_client - sha256: "9666ee14536778474072245ed5cba07db81ae8eb5de3b7bf4a2d1e2c49696092" - url: "https://pub.dev" - source: hosted - version: "1.1.2" + version: "1.3.3" ffi: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -180,18 +164,18 @@ packages: dependency: transitive description: name: functions_client - sha256: e63f49cd3b41727f47b3bde284a11a4ac62839e0604f64077d4257487510e484 + sha256: b410e4d609522357396cd84bb9a8f6e3a4561b5f7d3ce82267f6f1c2af42f16b url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.2" gotrue: dependency: transitive description: name: gotrue - sha256: "8703db795511f69194fe77125a0c838bbb6befc2f95717b6e40331784a8bdecb" + sha256: "04a6efacffd42773ed96dc752f19bb20a1fbc383e81ba82659072b775cf62912" url: "https://pub.dev" source: hosted - version: "2.8.4" + version: "2.12.0" gtk: dependency: transitive description: @@ -204,18 +188,18 @@ packages: dependency: transitive description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.4.0" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" intl: dependency: transitive description: @@ -224,14 +208,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" - js: - dependency: transitive - description: - name: js - sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf - url: "https://pub.dev" - source: hosted - version: "0.7.1" json_annotation: dependency: transitive description: @@ -252,18 +228,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -284,18 +260,18 @@ packages: dependency: "direct main" description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -308,18 +284,18 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mime: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "2.0.0" mutex: dependency: transitive description: @@ -332,34 +308,34 @@ packages: dependency: "direct main" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_provider: dependency: "direct main" description: name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.10" + version: "2.2.17" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -388,10 +364,10 @@ packages: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -404,48 +380,55 @@ packages: dependency: transitive description: name: postgrest - sha256: c4197238601c7c3103b03a4bb77f2050b17d0064bf8b968309421abdebbb7f0e + sha256: "10b81a23b1c829ccadf68c626b4d66666453a1474d24c563f313f5ca7851d575" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.4.2" powersync: dependency: "direct main" description: path: "../../packages/powersync" relative: true source: path - version: "1.9.2" + version: "1.15.0" + powersync_core: + dependency: "direct overridden" + description: + path: "../../packages/powersync_core" + relative: true + source: path + version: "1.5.0" powersync_flutter_libs: dependency: "direct overridden" description: path: "../../packages/powersync_flutter_libs" relative: true source: path - version: "0.4.2" + version: "0.4.10" pub_semver: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" realtime_client: dependency: transitive description: name: realtime_client - sha256: d897a65ee3b1b5ddc1cf606f0b83792262d38fd5679c2df7e38da29c977513da + sha256: "3a0a99b5bd0fc3b35e8ee846d9a22fa2c2117f7ef1cb73d1e5f08f6c3d09c4e9" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.5.0" retry: dependency: transitive description: @@ -466,26 +449,26 @@ packages: dependency: transitive description: name: shared_preferences - sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: a7e8467e9181cef109f601e3f65765685786c1a738a83d7fbbde377589c0d974 + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.10" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.4" shared_preferences_linux: dependency: transitive description: @@ -506,10 +489,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" shared_preferences_windows: dependency: transitive description: @@ -522,15 +505,15 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" sprintf: dependency: transitive description: @@ -543,98 +526,98 @@ packages: dependency: transitive description: name: sqlite3 - sha256: bb174b3ec2527f9c5f680f73a89af8149dd99782fbb56ea88ad0807c5638f2ed + sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e" url: "https://pub.dev" source: hosted - version: "2.4.7" + version: "2.7.5" sqlite3_flutter_libs: dependency: transitive description: name: sqlite3_flutter_libs - sha256: "62bbb4073edbcdf53f40c80775f33eea01d301b7b81417e5b3fb7395416258c1" + sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14" url: "https://pub.dev" source: hosted - version: "0.5.24" + version: "0.5.32" sqlite3_web: dependency: transitive description: name: sqlite3_web - sha256: f22d1dda7a40be0867984f55cdf5c2d599e5f05d3be4a642d78f38b38983f554 + sha256: "967e076442f7e1233bd7241ca61f3efe4c7fc168dac0f38411bdb3bdf471eb3c" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.3.1" sqlite_async: dependency: transitive description: name: sqlite_async - sha256: d66fb6e6d07c1a834743326c033029f75becbb1fad6823d709f921872abc3d5b + sha256: a60e8d5c8df8e694933bd5a312c38393e79ad77d784bb91c6f38ba627bfb7aec url: "https://pub.dev" source: hosted - version: "0.11.0" + version: "0.11.4" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" storage_client: dependency: transitive description: name: storage_client - sha256: "28c147c805304dbc2b762becd1fc26ee0cb621ace3732b9ae61ef979aab8b367" + sha256: "09bac4d75eea58e8113ca928e6655a09cc8059e6d1b472ee801f01fde815bcfc" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.4.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" supabase: dependency: transitive description: name: supabase - sha256: "4ed1cf3298f39865c05b2d8557f92eb131a9b9af70e32e218672a0afce01a6bc" + sha256: f00172f5f0b2148ea1c573f52862d50cacb6f353f579f741fa35e51704845958 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.7.0" supabase_flutter: dependency: "direct main" description: name: supabase_flutter - sha256: ff6ba3048fd47d831fdc0027d3efb99346d99b95becfcb406562454bd9b229c5 + sha256: d88eccf9e46e57129725a08e72a3109b6f780921fdc27fe3d7669a11ae80906b url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.9.0" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.4" timeago: dependency: "direct main" description: @@ -647,10 +630,10 @@ packages: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" universal_io: dependency: "direct main" description: @@ -663,42 +646,42 @@ packages: dependency: transitive description: name: url_launcher - sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: f0c73347dfcfa5b3db8bc06e1502668265d39c08f310c29bff4e28eea9699f79 + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" url: "https://pub.dev" source: hosted - version: "6.3.9" + version: "6.3.16" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.3" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: @@ -711,26 +694,26 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.1" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.4" uuid: dependency: transitive description: name: uuid - sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "4.4.2" + version: "4.5.1" vector_math: dependency: transitive description: @@ -743,58 +726,58 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "15.0.0" web: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web_socket: dependency: transitive description: name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" yaml: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" yet_another_json_isolate: dependency: transitive description: name: yet_another_json_isolate - sha256: "47ed3900e6b0e4dfe378811a4402e85b7fc126a7daa94f840fef65ea9c8e46f4" + sha256: fe45897501fa156ccefbfb9359c9462ce5dec092f05e8a56109db30be864f01e url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/demos/supabase-simple-chat/pubspec.yaml b/demos/supabase-simple-chat/pubspec.yaml index 3e563cf8..463278d2 100644 --- a/demos/supabase-simple-chat/pubspec.yaml +++ b/demos/supabase-simple-chat/pubspec.yaml @@ -37,7 +37,7 @@ dependencies: supabase_flutter: ^2.0.2 timeago: ^3.6.0 - powersync: ^1.9.3 + powersync: ^1.16.1 path_provider: ^2.1.1 path: ^1.8.3 logging: ^1.2.0 diff --git a/demos/supabase-todolist-drift/.gitignore b/demos/supabase-todolist-drift/.gitignore index b5e97dcc..f4850867 100644 --- a/demos/supabase-todolist-drift/.gitignore +++ b/demos/supabase-todolist-drift/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related diff --git a/demos/supabase-todolist-drift/README.md b/demos/supabase-todolist-drift/README.md index 7b8d632e..0fd2661a 100644 --- a/demos/supabase-todolist-drift/README.md +++ b/demos/supabase-todolist-drift/README.md @@ -1,6 +1,7 @@ # PowerSync + Supabase + Drift Flutter Demo: Todo List App This demo app is an extension of the [Supabase Todo List App](../supabase-todolist/README.md) and showcases how to set up and use the [drift_sqlite_async](https://pub.dev/packages/drift_sqlite_async) library (Drift ORM) with PowerSync. +This demo also uses [riverpod](https://riverpod.dev) to highlight best practices for state management. Notes about the Drift usage are [further below](#drift). @@ -52,5 +53,5 @@ Insert the credentials of your new Supabase and PowerSync projects into `lib/app The `database.g.dart` file containing the \_$AppDatabase class has to be generated if there are changes made to the `database.dart` file. - `dart run build_runner build` generates all the required code once. -- `dart run build_runner build --delete-conflicting-outputs` deletes previously generated files and generates the required code once. +- `dart run build_runner build -d` deletes previously generated files and generates the required code once. - `dart run build_runner watch` watches for changes in your sources and generates code with incremental rebuilds. This is better for development. diff --git a/demos/supabase-todolist-drift/analysis_options.yaml b/demos/supabase-todolist-drift/analysis_options.yaml index 8648a0cd..adcc25de 100644 --- a/demos/supabase-todolist-drift/analysis_options.yaml +++ b/demos/supabase-todolist-drift/analysis_options.yaml @@ -1,3 +1,7 @@ include: package:flutter_lints/flutter.yaml analyzer: exclude: [lib/**.g.dart] + +linter: + rules: + - prefer_relative_imports diff --git a/demos/supabase-todolist-drift/database.sql b/demos/supabase-todolist-drift/database.sql new file mode 100644 index 00000000..eecc9768 --- /dev/null +++ b/demos/supabase-todolist-drift/database.sql @@ -0,0 +1,71 @@ +-- Create tables +create table + public.lists ( + id uuid not null default gen_random_uuid (), + created_at timestamp with time zone not null default now(), + name text not null, + owner_id uuid not null, + constraint lists_pkey primary key (id), + constraint lists_owner_id_fkey foreign key (owner_id) references auth.users (id) on delete cascade + ) tablespace pg_default; + +create table + public.todos ( + id uuid not null default gen_random_uuid (), + created_at timestamp with time zone not null default now(), + completed_at timestamp with time zone null, + description text not null, + completed boolean not null default false, + created_by uuid null, + completed_by uuid null, + list_id uuid not null, + photo_id uuid null, + constraint todos_pkey primary key (id), + constraint todos_created_by_fkey foreign key (created_by) references auth.users (id) on delete set null, + constraint todos_completed_by_fkey foreign key (completed_by) references auth.users (id) on delete set null, + constraint todos_list_id_fkey foreign key (list_id) references lists (id) on delete cascade + ) tablespace pg_default; + +-- Create publication for powersync +create publication powersync for table lists, todos; + +-- Set up Row Level Security (RLS) +-- See https://supabase.com/docs/guides/auth/row-level-security for more details. +alter table public.lists + enable row level security; + +alter table public.todos + enable row level security; + +create policy "owned lists" on public.lists for ALL using ( + auth.uid() = owner_id +); + +create policy "todos in owned lists" on public.todos for ALL using ( + auth.uid() IN ( + SELECT lists.owner_id FROM lists WHERE (lists.id = todos.list_id) + ) +); + +-- This trigger automatically creates some sample data when a user registers. +-- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details. +create function public.handle_new_user_sample_data() +returns trigger as $$ +declare + new_list_id uuid; +begin + insert into public.lists (name, owner_id) + values ('Shopping list', new.id) + returning id into new_list_id; + + insert into public.todos(description, list_id, created_by) + values ('Bread', new_list_id, new.id); + + insert into public.todos(description, list_id, created_by) + values ('Apples', new_list_id, new.id); + + return new; +end; +$$ language plpgsql security definer; + +create trigger new_user_sample_data after insert on auth.users for each row execute procedure public.handle_new_user_sample_data(); diff --git a/demos/supabase-todolist-drift/ios/Flutter/AppFrameworkInfo.plist b/demos/supabase-todolist-drift/ios/Flutter/AppFrameworkInfo.plist index 7c569640..1dc6cf76 100644 --- a/demos/supabase-todolist-drift/ios/Flutter/AppFrameworkInfo.plist +++ b/demos/supabase-todolist-drift/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/demos/supabase-todolist-drift/ios/Podfile.lock b/demos/supabase-todolist-drift/ios/Podfile.lock deleted file mode 100644 index 58930a79..00000000 --- a/demos/supabase-todolist-drift/ios/Podfile.lock +++ /dev/null @@ -1,85 +0,0 @@ -PODS: - - app_links (0.0.1): - - Flutter - - camera_avfoundation (0.0.1): - - Flutter - - Flutter (1.0.0) - - path_provider_foundation (0.0.1): - - Flutter - - FlutterMacOS - - powersync-sqlite-core (0.3.0) - - powersync_flutter_libs (0.0.1): - - Flutter - - powersync-sqlite-core (~> 0.3.0) - - shared_preferences_foundation (0.0.1): - - Flutter - - FlutterMacOS - - "sqlite3 (3.46.0+1)": - - "sqlite3/common (= 3.46.0+1)" - - "sqlite3/common (3.46.0+1)" - - "sqlite3/dbstatvtab (3.46.0+1)": - - sqlite3/common - - "sqlite3/fts5 (3.46.0+1)": - - sqlite3/common - - "sqlite3/perf-threadsafe (3.46.0+1)": - - sqlite3/common - - "sqlite3/rtree (3.46.0+1)": - - sqlite3/common - - sqlite3_flutter_libs (0.0.1): - - Flutter - - "sqlite3 (~> 3.46.0+1)" - - sqlite3/dbstatvtab - - sqlite3/fts5 - - sqlite3/perf-threadsafe - - sqlite3/rtree - - url_launcher_ios (0.0.1): - - Flutter - -DEPENDENCIES: - - app_links (from `.symlinks/plugins/app_links/ios`) - - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) - - Flutter (from `Flutter`) - - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - - powersync_flutter_libs (from `.symlinks/plugins/powersync_flutter_libs/ios`) - - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) - - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - -SPEC REPOS: - trunk: - - powersync-sqlite-core - - sqlite3 - -EXTERNAL SOURCES: - app_links: - :path: ".symlinks/plugins/app_links/ios" - camera_avfoundation: - :path: ".symlinks/plugins/camera_avfoundation/ios" - Flutter: - :path: Flutter - path_provider_foundation: - :path: ".symlinks/plugins/path_provider_foundation/darwin" - powersync_flutter_libs: - :path: ".symlinks/plugins/powersync_flutter_libs/ios" - shared_preferences_foundation: - :path: ".symlinks/plugins/shared_preferences_foundation/darwin" - sqlite3_flutter_libs: - :path: ".symlinks/plugins/sqlite3_flutter_libs/ios" - url_launcher_ios: - :path: ".symlinks/plugins/url_launcher_ios/ios" - -SPEC CHECKSUMS: - app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795 - camera_avfoundation: dd002b0330f4981e1bbcb46ae9b62829237459a4 - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - powersync-sqlite-core: ad0e70e23bacd858fe2e79032dc4aabdf972d1bd - powersync_flutter_libs: 064c44b51fb07df9486b735fb96ab7608a89e18b - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sqlite3: 292c3e1bfe89f64e51ea7fc7dab9182a017c8630 - sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b - url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe - -PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 - -COCOAPODS: 1.15.2 diff --git a/demos/supabase-todolist-drift/ios/Runner.xcodeproj/project.pbxproj b/demos/supabase-todolist-drift/ios/Runner.xcodeproj/project.pbxproj index 94547304..ad2605c8 100644 --- a/demos/supabase-todolist-drift/ios/Runner.xcodeproj/project.pbxproj +++ b/demos/supabase-todolist-drift/ios/Runner.xcodeproj/project.pbxproj @@ -8,11 +8,10 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 2AA95F4BD1106733AA65B3D1 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C7F26FE8A2AE7EB1DCD03A63 /* Pods_RunnerTests.framework */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 6B669B18D5922DF2E9FF3231 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DC37C372644D968F6312083 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -44,16 +43,12 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 15AA756A195A0498929C7AB8 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 4ABD541213112FCC0D84BBA6 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - 53F1849573A471693AAF7757 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - 5AB1DDF473ECE5996A267CFC /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 5DC37C372644D968F6312083 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; @@ -62,9 +57,6 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - A15106F56CD7643EC0938932 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - AC1EC468FE1100F19B83C528 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - C7F26FE8A2AE7EB1DCD03A63 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -72,7 +64,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 2AA95F4BD1106733AA65B3D1 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -80,7 +71,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 6B669B18D5922DF2E9FF3231 /* Pods_Runner.framework in Frameworks */, + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -95,23 +86,10 @@ path = RunnerTests; sourceTree = ""; }; - 775FDDC091D1B0F1643D5E75 /* Pods */ = { - isa = PBXGroup; - children = ( - 5AB1DDF473ECE5996A267CFC /* Pods-Runner.debug.xcconfig */, - A15106F56CD7643EC0938932 /* Pods-Runner.release.xcconfig */, - AC1EC468FE1100F19B83C528 /* Pods-Runner.profile.xcconfig */, - 53F1849573A471693AAF7757 /* Pods-RunnerTests.debug.xcconfig */, - 15AA756A195A0498929C7AB8 /* Pods-RunnerTests.release.xcconfig */, - 4ABD541213112FCC0D84BBA6 /* Pods-RunnerTests.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -127,8 +105,6 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, - 775FDDC091D1B0F1643D5E75 /* Pods */, - CD2D4C39902602D7512B2E46 /* Frameworks */, ); sourceTree = ""; }; @@ -156,15 +132,6 @@ path = Runner; sourceTree = ""; }; - CD2D4C39902602D7512B2E46 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 5DC37C372644D968F6312083 /* Pods_Runner.framework */, - C7F26FE8A2AE7EB1DCD03A63 /* Pods_RunnerTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -172,7 +139,6 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 1BFE7CDE33D6F794EB1D704F /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, 2127B9F9BB1BBDC30AACA133 /* Frameworks */, @@ -191,20 +157,21 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 7978750E9AC7A32658AF3F71 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 25F5BBE07D381880BF0BA164 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -238,6 +205,9 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -270,45 +240,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 1BFE7CDE33D6F794EB1D704F /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 25F5BBE07D381880BF0BA164 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -325,28 +256,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 7978750E9AC7A32658AF3F71 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -455,7 +364,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -488,7 +397,6 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 53F1849573A471693AAF7757 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -506,7 +414,6 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 15AA756A195A0498929C7AB8 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -522,7 +429,6 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 4ABD541213112FCC0D84BBA6 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -585,7 +491,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -636,7 +542,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -726,6 +632,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/demos/supabase-todolist-drift/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/demos/supabase-todolist-drift/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..8e5eb05f --- /dev/null +++ b/demos/supabase-todolist-drift/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,22 @@ +{ + "pins" : [ + { + "identity" : "csqlite", + "kind" : "remoteSourceControl", + "location" : "https://github.com/simolus3/CSQLite.git", + "state" : { + "revision" : "a8d28afef08ad8faa4ee9ef7845f61c2e8ac5810" + } + }, + { + "identity" : "powersync-sqlite-core-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/powersync-ja/powersync-sqlite-core-swift.git", + "state" : { + "revision" : "00776db5157c8648671b00e6673603144fafbfeb", + "version" : "0.4.5" + } + } + ], + "version" : 2 +} diff --git a/demos/supabase-todolist-drift/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demos/supabase-todolist-drift/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 8e3ca5df..c3fedb29 100644 --- a/demos/supabase-todolist-drift/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/demos/supabase-todolist-drift/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + diff --git a/demos/supabase-todolist-drift/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/demos/supabase-todolist-drift/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..0c12c1e5 --- /dev/null +++ b/demos/supabase-todolist-drift/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,22 @@ +{ + "pins" : [ + { + "identity" : "csqlite", + "kind" : "remoteSourceControl", + "location" : "https://github.com/simolus3/CSQLite.git", + "state" : { + "revision" : "a268235ae86718e66d6a29feef3bd22c772eb82b" + } + }, + { + "identity" : "powersync-sqlite-core-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/powersync-ja/powersync-sqlite-core-swift.git", + "state" : { + "revision" : "b2a81af14e9ad83393eb187bb02e62e6db8b5ad6", + "version" : "0.4.6" + } + } + ], + "version" : 2 +} diff --git a/demos/supabase-todolist-drift/ios/Runner/AppDelegate.swift b/demos/supabase-todolist-drift/ios/Runner/AppDelegate.swift index 9074fee9..62666446 100644 --- a/demos/supabase-todolist-drift/ios/Runner/AppDelegate.swift +++ b/demos/supabase-todolist-drift/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import Flutter import UIKit -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/demos/supabase-todolist-drift/lib/attachments/camera_helpers.dart b/demos/supabase-todolist-drift/lib/attachments/camera_helpers.dart deleted file mode 100644 index 8aab6291..00000000 --- a/demos/supabase-todolist-drift/lib/attachments/camera_helpers.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:camera/camera.dart'; -import 'package:flutter/widgets.dart'; -import 'package:supabase_todolist_drift/powersync.dart'; - -Future setupCamera() async { - // Ensure that plugin services are initialized so that `availableCameras()` - // can be called before `runApp()` - WidgetsFlutterBinding.ensureInitialized(); - // Obtain a list of the available cameras on the device. - try { - final cameras = await availableCameras(); - // Get a specific camera from the list of available cameras. - final camera = cameras.isNotEmpty ? cameras.first : null; - return camera; - } catch (e) { - // Camera is not supported on all platforms - log.warning('Failed to setup camera: $e'); - return null; - } -} diff --git a/demos/supabase-todolist-drift/lib/attachments/photo_widget.dart b/demos/supabase-todolist-drift/lib/attachments/photo_widget.dart deleted file mode 100644 index 7d3799b7..00000000 --- a/demos/supabase-todolist-drift/lib/attachments/photo_widget.dart +++ /dev/null @@ -1,132 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:powersync_attachments_helper/powersync_attachments_helper.dart'; -import 'package:supabase_todolist_drift/attachments/camera_helpers.dart'; -import 'package:supabase_todolist_drift/attachments/photo_capture_widget.dart'; -import 'package:supabase_todolist_drift/attachments/queue.dart'; -import 'package:supabase_todolist_drift/database.dart'; - -class PhotoWidget extends StatefulWidget { - final TodoItem todo; - - PhotoWidget({ - required this.todo, - }) : super(key: ObjectKey(todo.id)); - - @override - State createState() { - return _PhotoWidgetState(); - } -} - -class _ResolvedPhotoState { - String? photoPath; - bool fileExists; - Attachment? attachment; - - _ResolvedPhotoState( - {required this.photoPath, required this.fileExists, this.attachment}); -} - -class _PhotoWidgetState extends State { - late String photoPath; - - Future<_ResolvedPhotoState> _getPhotoState(photoId) async { - if (photoId == null) { - return _ResolvedPhotoState(photoPath: null, fileExists: false); - } - photoPath = await attachmentQueue.getLocalUri('$photoId.jpg'); - - bool fileExists = await File(photoPath).exists(); - - final row = await attachmentQueue.db - .getOptional('SELECT * FROM attachments_queue WHERE id = ?', [photoId]); - - if (row != null) { - Attachment attachment = Attachment.fromRow(row); - return _ResolvedPhotoState( - photoPath: photoPath, fileExists: fileExists, attachment: attachment); - } - - return _ResolvedPhotoState( - photoPath: photoPath, fileExists: fileExists, attachment: null); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: _getPhotoState(widget.todo.photoId), - builder: (BuildContext context, - AsyncSnapshot<_ResolvedPhotoState> snapshot) { - if (snapshot.data == null) { - return Container(); - } - final data = snapshot.data!; - Widget takePhotoButton = ElevatedButton( - onPressed: () async { - final camera = await setupCamera(); - if (!context.mounted) return; - - if (camera == null) { - const snackBar = SnackBar( - content: Text('No camera available'), - backgroundColor: - Colors.red, // Optional: to highlight it's an error - ); - - ScaffoldMessenger.of(context).showSnackBar(snackBar); - return; - } - - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - TakePhotoWidget(todoId: widget.todo.id, camera: camera), - ), - ); - }, - child: const Text('Take Photo'), - ); - - if (widget.todo.photoId == null) { - return takePhotoButton; - } - - String? filePath = data.photoPath; - bool fileIsDownloading = !data.fileExists; - bool fileArchived = - data.attachment?.state == AttachmentState.archived.index; - - if (fileArchived) { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text("Unavailable"), - const SizedBox(height: 8), - takePhotoButton - ], - ); - } - - if (fileIsDownloading) { - return const Text("Downloading..."); - } - - File imageFile = File(filePath!); - int lastModified = imageFile.existsSync() - ? imageFile.lastModifiedSync().millisecondsSinceEpoch - : 0; - Key key = ObjectKey('$filePath:$lastModified'); - - return Image.file( - key: key, - imageFile, - width: 50, - height: 50, - ); - }); - } -} diff --git a/demos/supabase-todolist-drift/lib/widgets/status_app_bar.dart b/demos/supabase-todolist-drift/lib/components/app_bar.dart similarity index 67% rename from demos/supabase-todolist-drift/lib/widgets/status_app_bar.dart rename to demos/supabase-todolist-drift/lib/components/app_bar.dart index 75098d69..79de9a32 100644 --- a/demos/supabase-todolist-drift/lib/widgets/status_app_bar.dart +++ b/demos/supabase-todolist-drift/lib/components/app_bar.dart @@ -1,50 +1,32 @@ -import 'dart:async'; - +import 'package:auto_route/auto_route.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:powersync/powersync.dart'; -import 'package:supabase_todolist_drift/widgets/fts_search_delegate.dart'; -import '../powersync.dart'; - -class StatusAppBar extends StatefulWidget implements PreferredSizeWidget { - const StatusAppBar({super.key, required this.title}); - final String title; +import '../powersync/powersync.dart'; +import '../screens/search.dart'; - @override - State createState() => _StatusAppBarState(); +final appBar = AppBar( + title: const Text('PowerSync Flutter Demo'), +); - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} +final class StatusAppBar extends ConsumerWidget implements PreferredSizeWidget { + final Widget title; -class _StatusAppBarState extends State { - late SyncStatus _connectionState; - StreamSubscription? _syncStatusSubscription; - - @override - void initState() { - super.initState(); - _connectionState = db.currentStatus; - _syncStatusSubscription = db.statusStream.listen((event) { - setState(() { - _connectionState = db.currentStatus; - }); - }); - } + const StatusAppBar({super.key, required this.title}); @override - void dispose() { - super.dispose(); - _syncStatusSubscription?.cancel(); - } + Size get preferredSize => const Size.fromHeight(kToolbarHeight); @override - Widget build(BuildContext context) { - final statusIcon = _getStatusIcon(_connectionState); + Widget build(BuildContext context, WidgetRef ref) { + final syncState = ref.watch(syncStatus); + final statusIcon = _getStatusIcon(syncState); return AppBar( - title: Text(widget.title), + leading: const AutoLeadingButton(), + title: title, actions: [ IconButton( onPressed: () { diff --git a/demos/supabase-todolist-drift/lib/components/page_layout.dart b/demos/supabase-todolist-drift/lib/components/page_layout.dart new file mode 100644 index 00000000..523d6c93 --- /dev/null +++ b/demos/supabase-todolist-drift/lib/components/page_layout.dart @@ -0,0 +1,67 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../navigation.gr.dart'; +import '../supabase.dart'; +import 'app_bar.dart'; + +final class PageLayout extends ConsumerWidget { + final Widget content; + final Widget? title; + final Widget? floatingActionButton; + final bool showDrawer; + + const PageLayout({ + super.key, + required this.content, + this.title, + this.floatingActionButton, + this.showDrawer = true, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + appBar: StatusAppBar( + title: title ?? const Text('PowerSync Demo'), + ), + body: Center(child: content), + floatingActionButton: floatingActionButton, + drawer: showDrawer + ? Drawer( + // Add a ListView to the drawer. This ensures the user can scroll + // through the options in the drawer if there isn't enough vertical + // space to fit everything. + child: ListView( + // Important: Remove any padding from the ListView. + padding: EdgeInsets.zero, + children: [ + const DrawerHeader( + decoration: BoxDecoration( + color: Colors.blue, + ), + child: Text(''), + ), + ListTile( + title: const Text('SQL Console'), + onTap: () { + final route = context.topRoute; + if (route.name != SqlConsoleRoute.name) { + context.pushRoute(const SqlConsoleRoute()); + } + }, + ), + ListTile( + title: const Text('Sign Out'), + onTap: () async { + ref.read(authNotifierProvider.notifier).signOut(); + }, + ), + ], + ), + ) + : null, + ); + } +} diff --git a/demos/supabase-todolist-drift/lib/components/photo_widget.dart b/demos/supabase-todolist-drift/lib/components/photo_widget.dart new file mode 100644 index 00000000..2b007717 --- /dev/null +++ b/demos/supabase-todolist-drift/lib/components/photo_widget.dart @@ -0,0 +1,139 @@ +import 'dart:io'; + +import 'package:auto_route/auto_route.dart'; +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logging/logging.dart'; +import 'package:powersync_attachments_helper/powersync_attachments_helper.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../navigation.dart'; +import '../powersync/attachments/queue.dart'; +import '../powersync/database.dart'; + +part 'photo_widget.g.dart'; + +final class PhotoWidget extends ConsumerWidget { + final TodoItem todo; + + const PhotoWidget({super.key, required this.todo}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final photoState = ref.watch(_getPhotoStateProvider(todo.photoId)); + if (!photoState.hasValue) { + return Container(); + } + + final data = photoState.requireValue; + Widget takePhotoButton = ElevatedButton( + onPressed: () async { + final camera = await setupCamera(); + if (!context.mounted) return; + + if (camera == null) { + const snackBar = SnackBar( + content: Text('No camera available'), + backgroundColor: Colors.red, // Optional: to highlight it's an error + ); + + ScaffoldMessenger.of(context).showSnackBar(snackBar); + return; + } + + context.pushRoute(TakePhotoRoute(todoId: todo.id, camera: camera)); + }, + child: const Text('Take Photo'), + ); + + if (todo.photoId == null) { + return takePhotoButton; + } + + String? filePath = data.photoPath; + bool fileIsDownloading = !data.fileExists; + bool fileArchived = + data.attachment?.state == AttachmentState.archived.index; + + if (fileArchived) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text("Unavailable"), + const SizedBox(height: 8), + takePhotoButton + ], + ); + } + + if (fileIsDownloading) { + return const Text("Downloading..."); + } + + File imageFile = File(filePath!); + int lastModified = imageFile.existsSync() + ? imageFile.lastModifiedSync().millisecondsSinceEpoch + : 0; + Key key = ObjectKey('$filePath:$lastModified'); + + return Image.file( + key: key, + imageFile, + width: 50, + height: 50, + ); + } +} + +class _ResolvedPhotoState { + String? photoPath; + bool fileExists; + Attachment? attachment; + + _ResolvedPhotoState( + {required this.photoPath, required this.fileExists, this.attachment}); +} + +@riverpod +Future<_ResolvedPhotoState> _getPhotoState(Ref ref, String? photoId) async { + if (photoId == null) { + return _ResolvedPhotoState(photoPath: null, fileExists: false); + } + final queue = await ref.read(attachmentQueueProvider.future); + final photoPath = await queue.getLocalUri('$photoId.jpg'); + + bool fileExists = await File(photoPath).exists(); + + final row = await queue.db + .getOptional('SELECT * FROM attachments_queue WHERE id = ?', [photoId]); + + if (row != null) { + Attachment attachment = Attachment.fromRow(row); + return _ResolvedPhotoState( + photoPath: photoPath, fileExists: fileExists, attachment: attachment); + } + + return _ResolvedPhotoState( + photoPath: photoPath, fileExists: fileExists, attachment: null); +} + +final _log = Logger('setupCamera'); + +Future setupCamera() async { + // Ensure that plugin services are initialized so that `availableCameras()` + // can be called before `runApp()` + WidgetsFlutterBinding.ensureInitialized(); + // Obtain a list of the available cameras on the device. + try { + final cameras = await availableCameras(); + // Get a specific camera from the list of available cameras. + final camera = cameras.isNotEmpty ? cameras.first : null; + return camera; + } catch (e) { + // Camera is not supported on all platforms + _log.warning('Failed to setup camera: $e'); + return null; + } +} diff --git a/demos/supabase-todolist-drift/lib/components/photo_widget.g.dart b/demos/supabase-todolist-drift/lib/components/photo_widget.g.dart new file mode 100644 index 00000000..84441775 --- /dev/null +++ b/demos/supabase-todolist-drift/lib/components/photo_widget.g.dart @@ -0,0 +1,162 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'photo_widget.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$getPhotoStateHash() => r'9dd805dcfabe9288a1e8c125bae75c34d29c494b'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [_getPhotoState]. +@ProviderFor(_getPhotoState) +const _getPhotoStateProvider = _GetPhotoStateFamily(); + +/// See also [_getPhotoState]. +class _GetPhotoStateFamily extends Family> { + /// See also [_getPhotoState]. + const _GetPhotoStateFamily(); + + /// See also [_getPhotoState]. + _GetPhotoStateProvider call( + String? photoId, + ) { + return _GetPhotoStateProvider( + photoId, + ); + } + + @override + _GetPhotoStateProvider getProviderOverride( + covariant _GetPhotoStateProvider provider, + ) { + return call( + provider.photoId, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'_getPhotoStateProvider'; +} + +/// See also [_getPhotoState]. +class _GetPhotoStateProvider + extends AutoDisposeFutureProvider<_ResolvedPhotoState> { + /// See also [_getPhotoState]. + _GetPhotoStateProvider( + String? photoId, + ) : this._internal( + (ref) => _getPhotoState( + ref as _GetPhotoStateRef, + photoId, + ), + from: _getPhotoStateProvider, + name: r'_getPhotoStateProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$getPhotoStateHash, + dependencies: _GetPhotoStateFamily._dependencies, + allTransitiveDependencies: + _GetPhotoStateFamily._allTransitiveDependencies, + photoId: photoId, + ); + + _GetPhotoStateProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.photoId, + }) : super.internal(); + + final String? photoId; + + @override + Override overrideWith( + FutureOr<_ResolvedPhotoState> Function(_GetPhotoStateRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: _GetPhotoStateProvider._internal( + (ref) => create(ref as _GetPhotoStateRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + photoId: photoId, + ), + ); + } + + @override + AutoDisposeFutureProviderElement<_ResolvedPhotoState> createElement() { + return _GetPhotoStateProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is _GetPhotoStateProvider && other.photoId == photoId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, photoId.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin _GetPhotoStateRef on AutoDisposeFutureProviderRef<_ResolvedPhotoState> { + /// The parameter `photoId` of this provider. + String? get photoId; +} + +class _GetPhotoStateProviderElement + extends AutoDisposeFutureProviderElement<_ResolvedPhotoState> + with _GetPhotoStateRef { + _GetPhotoStateProviderElement(super.provider); + + @override + String? get photoId => (origin as _GetPhotoStateProvider).photoId; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/demos/supabase-todolist-drift/lib/database.dart b/demos/supabase-todolist-drift/lib/database.dart deleted file mode 100644 index cf307d72..00000000 --- a/demos/supabase-todolist-drift/lib/database.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:powersync/powersync.dart' show PowerSyncDatabase, uuid; -import 'package:drift_sqlite_async/drift_sqlite_async.dart'; -import 'package:supabase_todolist_drift/powersync.dart'; - -part 'database.g.dart'; - -class TodoItems extends Table { - @override - String get tableName => 'todos'; - - TextColumn get id => text().clientDefault(() => uuid.v4())(); - TextColumn get listId => text().named('list_id').references(ListItems, #id)(); - TextColumn get photoId => text().nullable().named('photo_id')(); - DateTimeColumn get createdAt => dateTime().nullable().named('created_at')(); - DateTimeColumn get completedAt => - dateTime().nullable().named('completed_at')(); - BoolColumn get completed => boolean().nullable()(); - TextColumn get description => text()(); - TextColumn get createdBy => text().nullable().named('created_by')(); - TextColumn get completedBy => text().nullable().named('completed_by')(); -} - -class ListItems extends Table { - @override - String get tableName => 'lists'; - - TextColumn get id => text().clientDefault(() => uuid.v4())(); - DateTimeColumn get createdAt => - dateTime().named('created_at').clientDefault(() => DateTime.now())(); - TextColumn get name => text()(); - TextColumn get ownerId => text().nullable().named('owner_id')(); -} - -class ListItemWithStats { - late ListItem self; - int completedCount; - int pendingCount; - - ListItemWithStats( - this.self, - this.completedCount, - this.pendingCount, - ); -} - -@DriftDatabase(tables: [TodoItems, ListItems], include: {'queries.drift'}) -class AppDatabase extends _$AppDatabase { - AppDatabase(PowerSyncDatabase db) : super(SqliteAsyncDriftConnection(db)); - - @override - int get schemaVersion => 1; - - Stream> watchLists() { - return (select(listItems) - ..orderBy([(l) => OrderingTerm(expression: l.createdAt)])) - .watch(); - } - - Stream> watchListsWithStats() { - return listsWithStats().watch(); - } - - Future createList(String name) async { - return into(listItems).insertReturning( - ListItemsCompanion.insert(name: name, ownerId: Value(getUserId()))); - } - - Future deleteList(ListItem list) async { - await (delete(listItems)..where((t) => t.id.equals(list.id))).go(); - } - - Stream> watchTodoItems(ListItem list) { - return (select(todoItems) - ..where((t) => t.listId.equals(list.id)) - ..orderBy([(t) => OrderingTerm(expression: t.createdAt)])) - .watch(); - } - - Future deleteTodo(TodoItem todo) async { - await (delete(todoItems)..where((t) => t.id.equals(todo.id))).go(); - } - - Future addTodo(ListItem list, String description) async { - return into(todoItems).insertReturning(TodoItemsCompanion.insert( - listId: list.id, - description: description, - completed: const Value(false), - createdBy: Value(getUserId()))); - } - - Future toggleTodo(TodoItem todo) async { - if (todo.completed != true) { - await (update(todoItems)..where((t) => t.id.equals(todo.id))).write( - TodoItemsCompanion( - completed: const Value(true), - completedAt: Value(DateTime.now()), - completedBy: Value(getUserId()))); - } else { - await (update(todoItems)..where((t) => t.id.equals(todo.id))).write( - const TodoItemsCompanion( - completed: Value(false), - completedAt: Value.absent(), - completedBy: Value.absent())); - } - } - - Future addTodoPhoto(String todoId, String photoId) async { - await (update(todoItems)..where((t) => t.id.equals(todoId))) - .write(TodoItemsCompanion(photoId: Value(photoId))); - } - - Future findList(String id) { - return (select(listItems)..where((t) => t.id.equals(id))).getSingle(); - } -} diff --git a/demos/supabase-todolist-drift/lib/main.dart b/demos/supabase-todolist-drift/lib/main.dart index fe48f040..7cfea76a 100644 --- a/demos/supabase-todolist-drift/lib/main.dart +++ b/demos/supabase-todolist-drift/lib/main.dart @@ -1,16 +1,14 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:logging/logging.dart'; -import 'package:supabase_todolist_drift/app_config.dart'; -import 'package:supabase_todolist_drift/attachments/queue.dart'; -import 'package:supabase_todolist_drift/models/schema.dart'; -import 'powersync.dart'; -import 'widgets/lists_page.dart'; -import 'widgets/login_page.dart'; -import 'widgets/query_widget.dart'; -import 'widgets/signup_page.dart'; -import 'widgets/status_app_bar.dart'; +import 'navigation.dart'; +import 'supabase.dart'; +import 'utils/provider_observer.dart'; void main() async { Logger.root.level = Level.INFO; @@ -28,103 +26,43 @@ void main() async { } }); - WidgetsFlutterBinding - .ensureInitialized(); //required to get sqlite filepath from path_provider before UI has initialized - await openDatabase(); - - if (AppConfig.supabaseStorageBucket.isNotEmpty) { - initializeAttachmentQueue(db); - } - - final loggedIn = isLoggedIn(); - runApp(MyApp(loggedIn: loggedIn)); -} - -const defaultQuery = 'SELECT * from $todosTable'; - -const listsPage = ListsPage(); -const homePage = listsPage; - -const sqlConsolePage = Scaffold( - appBar: StatusAppBar(title: 'SQL Console'), - body: QueryWidget(defaultQuery: defaultQuery)); - -const loginPage = LoginPage(); + //required to get sqlite filepath from path_provider before UI has initialized + WidgetsFlutterBinding.ensureInitialized(); + await loadSupabase(); -const signupPage = SignupPage(); - -class MyApp extends StatelessWidget { - final bool loggedIn; - - const MyApp({super.key, required this.loggedIn}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'PowerSync Flutter Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: loggedIn ? homePage : loginPage); - } + runApp(const ProviderScope( + observers: [LoggingProviderObserver()], + child: MyApp(), + )); } -class MyHomePage extends StatelessWidget { - const MyHomePage( - {super.key, - required this.title, - required this.content, - this.floatingActionButton}); - - final String title; - final Widget content; - final Widget? floatingActionButton; +class MyApp extends HookConsumerWidget { + const MyApp({super.key}); @override - Widget build(BuildContext context) { - return Scaffold( - appBar: StatusAppBar(title: title), - body: Center(child: content), - floatingActionButton: floatingActionButton, - drawer: Drawer( - // Add a ListView to the drawer. This ensures the user can scroll - // through the options in the drawer if there isn't enough vertical - // space to fit everything. - child: ListView( - // Important: Remove any padding from the ListView. - padding: EdgeInsets.zero, - children: [ - const DrawerHeader( - decoration: BoxDecoration( - color: Colors.blue, - ), - child: Text(''), - ), - ListTile( - title: const Text('SQL Console'), - onTap: () { - var navigator = Navigator.of(context); - navigator.pop(); - - navigator.push(MaterialPageRoute( - builder: (context) => sqlConsolePage, - )); - }, - ), - ListTile( - title: const Text('Sign Out'), - onTap: () async { - var navigator = Navigator.of(context); - navigator.pop(); - await logout(); + Widget build(BuildContext context, WidgetRef ref) { + final router = ref.watch(appRouter); + + // Bridge riverpod session provider to the listenable that auto_route wants + // to re-evaluate route guards. + final sessionNotifier = useValueNotifier(ref.read(isLoggedInProvider)); + ref.listen(isLoggedInProvider, (prev, now) { + if (sessionNotifier.value != now) { + // Using Timer.run() here to work around an issue with auto_route during + // initialization. + Timer.run(() { + sessionNotifier.value = now; + }); + } + }); - navigator.pushReplacement(MaterialPageRoute( - builder: (context) => loginPage, - )); - }, - ), - ], - ), + return MaterialApp.router( + routerConfig: router.config( + reevaluateListenable: sessionNotifier, + ), + title: 'PowerSync Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, ), ); } diff --git a/demos/supabase-todolist-drift/lib/migrations/fts_setup.dart b/demos/supabase-todolist-drift/lib/migrations/fts_setup.dart deleted file mode 100644 index 9754e44e..00000000 --- a/demos/supabase-todolist-drift/lib/migrations/fts_setup.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:powersync/powersync.dart'; -import 'package:powersync/sqlite_async.dart'; - -import 'helpers.dart'; -import '../models/schema.dart'; - -final migrations = SqliteMigrations(); - -/// Create a Full Text Search table for the given table and columns -/// with an option to use a different tokenizer otherwise it defaults -/// to unicode61. It also creates the triggers that keep the FTS table -/// and the PowerSync table in sync. -SqliteMigration createFtsMigration( - {required int migrationVersion, - required String tableName, - required List columns, - String tokenizationMethod = 'unicode61'}) { - String internalName = - schema.tables.firstWhere((table) => table.name == tableName).internalName; - String stringColumns = columns.join(', '); - - return SqliteMigration(migrationVersion, (tx) async { - // Add FTS table - await tx.execute(''' - CREATE VIRTUAL TABLE IF NOT EXISTS fts_$tableName - USING fts5(id UNINDEXED, $stringColumns, tokenize='$tokenizationMethod'); - '''); - // Copy over records already in table - await tx.execute(''' - INSERT INTO fts_$tableName(rowid, id, $stringColumns) - SELECT rowid, id, ${generateJsonExtracts(ExtractType.columnOnly, 'data', columns)} FROM $internalName; - '''); - // Add INSERT, UPDATE and DELETE and triggers to keep fts table in sync with table - await tx.execute(''' - CREATE TRIGGER IF NOT EXISTS fts_insert_trigger_$tableName AFTER INSERT ON $internalName - BEGIN - INSERT INTO fts_$tableName(rowid, id, $stringColumns) - VALUES ( - NEW.rowid, - NEW.id, - ${generateJsonExtracts(ExtractType.columnOnly, 'NEW.data', columns)} - ); - END; - '''); - await tx.execute(''' - CREATE TRIGGER IF NOT EXISTS fts_update_trigger_$tableName AFTER UPDATE ON $internalName BEGIN - UPDATE fts_$tableName - SET ${generateJsonExtracts(ExtractType.columnInOperation, 'NEW.data', columns)} - WHERE rowid = NEW.rowid; - END; - '''); - await tx.execute(''' - CREATE TRIGGER IF NOT EXISTS fts_delete_trigger_$tableName AFTER DELETE ON $internalName BEGIN - DELETE FROM fts_$tableName WHERE rowid = OLD.rowid; - END; - '''); - }); -} - -/// This is where you can add more migrations to generate FTS tables -/// that correspond to the tables in your schema and populate them -/// with the data you would like to search on -Future configureFts(PowerSyncDatabase db) async { - migrations - ..add(createFtsMigration( - migrationVersion: 1, - tableName: 'lists', - columns: ['name'], - tokenizationMethod: 'porter unicode61')) - ..add(createFtsMigration( - migrationVersion: 2, - tableName: 'todos', - columns: ['description', 'list_id'], - )); - await migrations.migrate(db); -} diff --git a/demos/supabase-todolist-drift/lib/migrations/helpers.dart b/demos/supabase-todolist-drift/lib/migrations/helpers.dart deleted file mode 100644 index c1a779e1..00000000 --- a/demos/supabase-todolist-drift/lib/migrations/helpers.dart +++ /dev/null @@ -1,38 +0,0 @@ -typedef ExtractGenerator = String Function(String, String); - -enum ExtractType { - columnOnly, - columnInOperation, -} - -typedef ExtractGeneratorMap = Map; - -String _createExtract(String jsonColumnName, String columnName) => - 'json_extract($jsonColumnName, \'\$.$columnName\')'; - -ExtractGeneratorMap extractGeneratorsMap = { - ExtractType.columnOnly: ( - String jsonColumnName, - String columnName, - ) => - _createExtract(jsonColumnName, columnName), - ExtractType.columnInOperation: ( - String jsonColumnName, - String columnName, - ) => - '$columnName = ${_createExtract(jsonColumnName, columnName)}', -}; - -String generateJsonExtracts( - ExtractType type, String jsonColumnName, List columns) { - ExtractGenerator? generator = extractGeneratorsMap[type]; - if (generator == null) { - throw StateError('Unexpected null generator for key: $type'); - } - - if (columns.length == 1) { - return generator(jsonColumnName, columns.first); - } - - return columns.map((column) => generator(jsonColumnName, column)).join(', '); -} diff --git a/demos/supabase-todolist-drift/lib/navigation.dart b/demos/supabase-todolist-drift/lib/navigation.dart new file mode 100644 index 00000000..6649a8d2 --- /dev/null +++ b/demos/supabase-todolist-drift/lib/navigation.dart @@ -0,0 +1,82 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'navigation.gr.dart'; +import 'supabase.dart'; + +export 'navigation.gr.dart'; + +@AutoRouterConfig() +final class AppRouter extends RootStackRouter { + final _AuthGuard _authGuard; + + AppRouter(Ref ref) : _authGuard = _AuthGuard(ref); + + @override + RouteType get defaultRouteType => const RouteType.material(); + + @override + List get routes { + return [ + AutoRoute(page: LoginRoute.page), + AutoRoute(page: SignupRoute.page), + AutoRoute( + page: LoggedInRoot.page, + initial: true, + guards: [_authGuard], + children: [ + AutoRoute( + initial: true, + page: ListsRoute.page, + ), + _dialogRoute(AddListRoute.page), + AutoRoute(page: ListsDetailsRoute.page), + _dialogRoute(AddItemRoute.page), + AutoRoute(page: TakePhotoRoute.page), + AutoRoute(page: SqlConsoleRoute.page), + ], + ), + ]; + } + + static CustomRoute _dialogRoute(PageInfo page) { + return CustomRoute( + page: page, + customRouteBuilder: (context, child, page) { + return DialogRoute( + context: context, + builder: (_) => child, + settings: page, + ); + }, + ); + } +} + +@RoutePage(name: 'LoggedInRoot') +final class LoggedInContents extends StatelessWidget { + const LoggedInContents({super.key}); + + @override + Widget build(BuildContext context) { + return const AutoRouter(); + } +} + +final class _AuthGuard extends AutoRouteGuard { + final Ref _ref; + + _AuthGuard(this._ref); + + @override + void onNavigation(NavigationResolver resolver, StackRouter router) { + if (_ref.read(isLoggedInProvider)) { + resolver.next(true); + } else { + resolver.redirectUntil(const LoginRoute()); + } + } +} + +final appRouter = Provider((ref) => AppRouter(ref)); diff --git a/demos/supabase-todolist-drift/lib/navigation.gr.dart b/demos/supabase-todolist-drift/lib/navigation.gr.dart new file mode 100644 index 00000000..27b35a2d --- /dev/null +++ b/demos/supabase-todolist-drift/lib/navigation.gr.dart @@ -0,0 +1,241 @@ +// dart format width=80 +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// AutoRouterGenerator +// ************************************************************************** + +// ignore_for_file: type=lint +// coverage:ignore-file + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:auto_route/auto_route.dart' as _i10; +import 'package:camera/camera.dart' as _i12; +import 'package:flutter/material.dart' as _i11; +import 'package:supabase_todolist_drift/navigation.dart' as _i5; +import 'package:supabase_todolist_drift/screens/add_item_dialog.dart' as _i1; +import 'package:supabase_todolist_drift/screens/add_list_dialog.dart' as _i2; +import 'package:supabase_todolist_drift/screens/list_details.dart' as _i3; +import 'package:supabase_todolist_drift/screens/lists.dart' as _i4; +import 'package:supabase_todolist_drift/screens/login.dart' as _i6; +import 'package:supabase_todolist_drift/screens/signup.dart' as _i7; +import 'package:supabase_todolist_drift/screens/sql_console.dart' as _i8; +import 'package:supabase_todolist_drift/screens/take_photo.dart' as _i9; + +/// generated route for +/// [_i1.AddItemDialog] +class AddItemRoute extends _i10.PageRouteInfo { + AddItemRoute({ + _i11.Key? key, + required String list, + List<_i10.PageRouteInfo>? children, + }) : super( + AddItemRoute.name, + args: AddItemRouteArgs(key: key, list: list), + initialChildren: children, + ); + + static const String name = 'AddItemRoute'; + + static _i10.PageInfo page = _i10.PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return _i1.AddItemDialog(key: args.key, list: args.list); + }, + ); +} + +class AddItemRouteArgs { + const AddItemRouteArgs({this.key, required this.list}); + + final _i11.Key? key; + + final String list; + + @override + String toString() { + return 'AddItemRouteArgs{key: $key, list: $list}'; + } +} + +/// generated route for +/// [_i2.AddListDialog] +class AddListRoute extends _i10.PageRouteInfo { + const AddListRoute({List<_i10.PageRouteInfo>? children}) + : super(AddListRoute.name, initialChildren: children); + + static const String name = 'AddListRoute'; + + static _i10.PageInfo page = _i10.PageInfo( + name, + builder: (data) { + return const _i2.AddListDialog(); + }, + ); +} + +/// generated route for +/// [_i3.ListsDetailsPage] +class ListsDetailsRoute extends _i10.PageRouteInfo { + ListsDetailsRoute({ + _i11.Key? key, + required String list, + List<_i10.PageRouteInfo>? children, + }) : super( + ListsDetailsRoute.name, + args: ListsDetailsRouteArgs(key: key, list: list), + initialChildren: children, + ); + + static const String name = 'ListsDetailsRoute'; + + static _i10.PageInfo page = _i10.PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return _i3.ListsDetailsPage(key: args.key, list: args.list); + }, + ); +} + +class ListsDetailsRouteArgs { + const ListsDetailsRouteArgs({this.key, required this.list}); + + final _i11.Key? key; + + final String list; + + @override + String toString() { + return 'ListsDetailsRouteArgs{key: $key, list: $list}'; + } +} + +/// generated route for +/// [_i4.ListsPage] +class ListsRoute extends _i10.PageRouteInfo { + const ListsRoute({List<_i10.PageRouteInfo>? children}) + : super(ListsRoute.name, initialChildren: children); + + static const String name = 'ListsRoute'; + + static _i10.PageInfo page = _i10.PageInfo( + name, + builder: (data) { + return const _i4.ListsPage(); + }, + ); +} + +/// generated route for +/// [_i5.LoggedInContents] +class LoggedInRoot extends _i10.PageRouteInfo { + const LoggedInRoot({List<_i10.PageRouteInfo>? children}) + : super(LoggedInRoot.name, initialChildren: children); + + static const String name = 'LoggedInRoot'; + + static _i10.PageInfo page = _i10.PageInfo( + name, + builder: (data) { + return const _i5.LoggedInContents(); + }, + ); +} + +/// generated route for +/// [_i6.LoginPage] +class LoginRoute extends _i10.PageRouteInfo { + const LoginRoute({List<_i10.PageRouteInfo>? children}) + : super(LoginRoute.name, initialChildren: children); + + static const String name = 'LoginRoute'; + + static _i10.PageInfo page = _i10.PageInfo( + name, + builder: (data) { + return const _i6.LoginPage(); + }, + ); +} + +/// generated route for +/// [_i7.SignupPage] +class SignupRoute extends _i10.PageRouteInfo { + const SignupRoute({List<_i10.PageRouteInfo>? children}) + : super(SignupRoute.name, initialChildren: children); + + static const String name = 'SignupRoute'; + + static _i10.PageInfo page = _i10.PageInfo( + name, + builder: (data) { + return const _i7.SignupPage(); + }, + ); +} + +/// generated route for +/// [_i8.SqlConsolePage] +class SqlConsoleRoute extends _i10.PageRouteInfo { + const SqlConsoleRoute({List<_i10.PageRouteInfo>? children}) + : super(SqlConsoleRoute.name, initialChildren: children); + + static const String name = 'SqlConsoleRoute'; + + static _i10.PageInfo page = _i10.PageInfo( + name, + builder: (data) { + return const _i8.SqlConsolePage(); + }, + ); +} + +/// generated route for +/// [_i9.TakePhotoPage] +class TakePhotoRoute extends _i10.PageRouteInfo { + TakePhotoRoute({ + _i11.Key? key, + required String todoId, + required _i12.CameraDescription camera, + List<_i10.PageRouteInfo>? children, + }) : super( + TakePhotoRoute.name, + args: TakePhotoRouteArgs(key: key, todoId: todoId, camera: camera), + initialChildren: children, + ); + + static const String name = 'TakePhotoRoute'; + + static _i10.PageInfo page = _i10.PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return _i9.TakePhotoPage( + key: args.key, + todoId: args.todoId, + camera: args.camera, + ); + }, + ); +} + +class TakePhotoRouteArgs { + const TakePhotoRouteArgs({ + this.key, + required this.todoId, + required this.camera, + }); + + final _i11.Key? key; + + final String todoId; + + final _i12.CameraDescription camera; + + @override + String toString() { + return 'TakePhotoRouteArgs{key: $key, todoId: $todoId, camera: $camera}'; + } +} diff --git a/demos/supabase-todolist-drift/lib/attachments/queue.dart b/demos/supabase-todolist-drift/lib/powersync/attachments/queue.dart similarity index 82% rename from demos/supabase-todolist-drift/lib/attachments/queue.dart rename to demos/supabase-todolist-drift/lib/powersync/attachments/queue.dart index 63d903ac..63088735 100644 --- a/demos/supabase-todolist-drift/lib/attachments/queue.dart +++ b/demos/supabase-todolist-drift/lib/powersync/attachments/queue.dart @@ -1,14 +1,24 @@ import 'dart:async'; -import 'package:powersync/powersync.dart'; import 'package:powersync_attachments_helper/powersync_attachments_helper.dart'; -import 'package:supabase_todolist_drift/app_config.dart'; -import 'package:supabase_todolist_drift/attachments/remote_storage_adapter.dart'; +import 'package:riverpod/riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:supabase_todolist_drift/models/schema.dart'; +import '../../app_config.dart'; +import '../powersync.dart'; +import '../schema.dart'; +import 'remote_storage_adapter.dart'; + +part 'queue.g.dart'; + +@Riverpod(keepAlive: true) +Future attachmentQueue(Ref ref) async { + final db = await ref.read(powerSyncInstanceProvider.future); + final queue = PhotoAttachmentQueue(db, remoteStorage); + await queue.init(); + return queue; +} -/// Global reference to the queue -late final PhotoAttachmentQueue attachmentQueue; final remoteStorage = SupabaseStorageAdapter(); /// Function to handle errors when downloading attachments @@ -83,8 +93,3 @@ class PhotoAttachmentQueue extends AbstractAttachmentQueue { }); } } - -initializeAttachmentQueue(PowerSyncDatabase db) async { - attachmentQueue = PhotoAttachmentQueue(db, remoteStorage); - await attachmentQueue.init(); -} diff --git a/demos/supabase-todolist-drift/lib/powersync/attachments/queue.g.dart b/demos/supabase-todolist-drift/lib/powersync/attachments/queue.g.dart new file mode 100644 index 00000000..a9402d66 --- /dev/null +++ b/demos/supabase-todolist-drift/lib/powersync/attachments/queue.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'queue.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$attachmentQueueHash() => r'353be28d71ad41994abf783776a99881e0b51383'; + +/// See also [attachmentQueue]. +@ProviderFor(attachmentQueue) +final attachmentQueueProvider = FutureProvider.internal( + attachmentQueue, + name: r'attachmentQueueProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$attachmentQueueHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef AttachmentQueueRef = FutureProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/demos/supabase-todolist-drift/lib/attachments/remote_storage_adapter.dart b/demos/supabase-todolist-drift/lib/powersync/attachments/remote_storage_adapter.dart similarity index 96% rename from demos/supabase-todolist-drift/lib/attachments/remote_storage_adapter.dart rename to demos/supabase-todolist-drift/lib/powersync/attachments/remote_storage_adapter.dart index d8383e69..336c9d10 100644 --- a/demos/supabase-todolist-drift/lib/attachments/remote_storage_adapter.dart +++ b/demos/supabase-todolist-drift/lib/powersync/attachments/remote_storage_adapter.dart @@ -1,10 +1,11 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:powersync_attachments_helper/powersync_attachments_helper.dart'; -import 'package:supabase_todolist_drift/app_config.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:image/image.dart' as img; +import '../../app_config.dart'; + class SupabaseStorageAdapter implements AbstractRemoteStorageAdapter { @override Future uploadFile(String filename, File file, diff --git a/demos/supabase-todolist-drift/lib/powersync.dart b/demos/supabase-todolist-drift/lib/powersync/connector.dart similarity index 62% rename from demos/supabase-todolist-drift/lib/powersync.dart rename to demos/supabase-todolist-drift/lib/powersync/connector.dart index 71a16c8d..e6bccc92 100644 --- a/demos/supabase-todolist-drift/lib/powersync.dart +++ b/demos/supabase-todolist-drift/lib/powersync/connector.dart @@ -1,18 +1,10 @@ -// This file performs setup of the PowerSync database -import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; -import 'package:path/path.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:powersync/powersync.dart'; -import 'package:supabase_todolist_drift/database.dart'; -import 'package:supabase_todolist_drift/migrations/fts_setup.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; -import 'app_config.dart'; -import 'models/schema.dart'; -import 'supabase.dart'; +import '../app_config.dart'; -final log = Logger('powersync-supabase'); +final _log = Logger('powersync-supabase'); /// Postgres Response codes that we cannot recover from by retrying. final List fatalResponseCodes = [ @@ -121,7 +113,7 @@ class SupabaseConnector extends PowerSyncBackendConnector { /// Note that these errors typically indicate a bug in the application. /// If protecting against data loss is important, save the failing records /// elsewhere instead of discarding, and/or notify the user. - log.severe('Data upload error - discarding $lastOp', e); + _log.severe('Data upload error - discarding $lastOp', e); await transaction.complete(); } else { // Error may be retryable - e.g. network error or temporary server error. @@ -131,72 +123,3 @@ class SupabaseConnector extends PowerSyncBackendConnector { } } } - -/// Global reference to the database -late final PowerSyncDatabase db; -late final AppDatabase appDb; - -bool isLoggedIn() { - return Supabase.instance.client.auth.currentSession?.accessToken != null; -} - -/// id of the user currently logged in -String? getUserId() { - return Supabase.instance.client.auth.currentSession?.user.id; -} - -Future getDatabasePath() async { - const dbFilename = 'powersync-demo.db'; - // getApplicationSupportDirectory is not supported on Web - if (kIsWeb) { - return dbFilename; - } - final dir = await getApplicationSupportDirectory(); - return join(dir.path, dbFilename); -} - -Future openDatabase() async { - // Open the local database - db = PowerSyncDatabase( - schema: schema, path: await getDatabasePath(), logger: attachedLogger); - await db.initialize(); - // Initialize the Drift database - appDb = AppDatabase(db); - - await loadSupabase(); - - SupabaseConnector? currentConnector; - - if (isLoggedIn()) { - // If the user is already logged in, connect immediately. - // Otherwise, connect once logged in. - currentConnector = SupabaseConnector(); - db.connect(connector: currentConnector); - } - - Supabase.instance.client.auth.onAuthStateChange.listen((data) async { - final AuthChangeEvent event = data.event; - if (event == AuthChangeEvent.signedIn) { - // Connect to PowerSync when the user is signed in - currentConnector = SupabaseConnector(); - db.connect(connector: currentConnector!); - } else if (event == AuthChangeEvent.signedOut) { - // Implicit sign out - disconnect, but don't delete data - currentConnector = null; - await db.disconnect(); - } else if (event == AuthChangeEvent.tokenRefreshed) { - // Supabase token refreshed - trigger token refresh for PowerSync. - currentConnector?.prefetchCredentials(); - } - }); - - // Demo using SQLite Full-Text Search with PowerSync. - // See https://docs.powersync.com/usage-examples/full-text-search for more details - await configureFts(db); -} - -/// Explicit sign out - clear database and log out. -Future logout() async { - await Supabase.instance.client.auth.signOut(); - await db.disconnectAndClear(); -} diff --git a/demos/supabase-todolist-drift/lib/powersync/database.dart b/demos/supabase-todolist-drift/lib/powersync/database.dart new file mode 100644 index 00000000..a135ffd7 --- /dev/null +++ b/demos/supabase-todolist-drift/lib/powersync/database.dart @@ -0,0 +1,104 @@ +import 'package:drift/drift.dart'; +import 'package:drift_sqlite_async/drift_sqlite_async.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:powersync/powersync.dart' show uuid; + +import 'fts5.dart'; +import 'powersync.dart'; + +part 'database.g.dart'; + +class TodoItems extends Table { + @override + String get tableName => 'todos'; + + TextColumn get id => text().clientDefault(() => uuid.v4())(); + TextColumn get listId => text().named('list_id').references(ListItems, #id)(); + TextColumn get photoId => text().nullable().named('photo_id')(); + DateTimeColumn get createdAt => dateTime().nullable().named('created_at')(); + DateTimeColumn get completedAt => + dateTime().nullable().named('completed_at')(); + BoolColumn get completed => boolean().nullable()(); + TextColumn get description => text()(); + TextColumn get createdBy => text().nullable().named('created_by')(); + TextColumn get completedBy => text().nullable().named('completed_by')(); +} + +class ListItems extends Table { + @override + String get tableName => 'lists'; + + TextColumn get id => text().clientDefault(() => uuid.v4())(); + DateTimeColumn get createdAt => + dateTime().named('created_at').clientDefault(() => DateTime.now())(); + TextColumn get name => text()(); + TextColumn get ownerId => text().nullable().named('owner_id')(); +} + +final class ListItemWithStats { + final ListItem self; + final int completedCount; + final int pendingCount; + + const ListItemWithStats( + this.self, + this.completedCount, + this.pendingCount, + ); +} + +@DriftDatabase( + tables: [TodoItems, ListItems], + include: {'queries.drift'}, +) +class AppDatabase extends _$AppDatabase { + AppDatabase(super.e); + + @override + int get schemaVersion => 2; + + @override + MigrationStrategy get migration { + return MigrationStrategy( + onCreate: (m) async { + // We don't have to call createAll(), PowerSync instantiates the schema + // for us. We can use the opportunity to create fts5 indexes though. + await createFts5Tables( + db: this, + tableName: 'lists', + columns: ['name'], + ); + await createFts5Tables( + db: this, + tableName: 'todos', + columns: ['description', 'list_id'], + ); + }, + onUpgrade: (m, from, to) async { + if (from == 1) { + await createFts5Tables( + db: this, + tableName: 'todos', + columns: ['description', 'list_id'], + ); + } + }, + ); + } + + Future addTodoPhoto(String todoId, String photoId) async { + await (update(todoItems)..where((t) => t.id.equals(todoId))) + .write(TodoItemsCompanion(photoId: Value(photoId))); + } + + Future findList(String id) { + return (select(listItems)..where((t) => t.id.equals(id))).getSingle(); + } +} + +final driftDatabase = Provider((ref) { + return AppDatabase(DatabaseConnection.delayed(Future(() async { + final database = await ref.read(powerSyncInstanceProvider.future); + return SqliteAsyncDriftConnection(database); + }))); +}); diff --git a/demos/supabase-todolist-drift/lib/database.g.dart b/demos/supabase-todolist-drift/lib/powersync/database.g.dart similarity index 78% rename from demos/supabase-todolist-drift/lib/database.g.dart rename to demos/supabase-todolist-drift/lib/powersync/database.g.dart index 438ea554..c709dc46 100644 --- a/demos/supabase-todolist-drift/lib/database.g.dart +++ b/demos/supabase-todolist-drift/lib/powersync/database.g.dart @@ -814,7 +814,7 @@ final class $$ListItemsTableReferences $$TodoItemsTableProcessedTableManager get todoItemsRefs { final manager = $$TodoItemsTableTableManager($_db, $_db.todoItems) - .filter((f) => f.listId.id($_item.id)); + .filter((f) => f.listId.id.sqlEquals($_itemColumn('id')!)); final cache = $_typedResult.readTableOrNull(_todoItemsRefsTable($_db)); return ProcessedTableManager( @@ -823,64 +823,111 @@ final class $$ListItemsTableReferences } class $$ListItemsTableFilterComposer - extends FilterComposer<_$AppDatabase, $ListItemsTable> { - $$ListItemsTableFilterComposer(super.$state); - ColumnFilters get id => $state.composableBuilder( - column: $state.table.id, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); - - ColumnFilters get createdAt => $state.composableBuilder( - column: $state.table.createdAt, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); - - ColumnFilters get name => $state.composableBuilder( - column: $state.table.name, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); - - ColumnFilters get ownerId => $state.composableBuilder( - column: $state.table.ownerId, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); - - ComposableFilter todoItemsRefs( - ComposableFilter Function($$TodoItemsTableFilterComposer f) f) { - final $$TodoItemsTableFilterComposer composer = $state.composerBuilder( + extends Composer<_$AppDatabase, $ListItemsTable> { + $$ListItemsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnFilters(column)); + + ColumnFilters get ownerId => $composableBuilder( + column: $table.ownerId, builder: (column) => ColumnFilters(column)); + + Expression todoItemsRefs( + Expression Function($$TodoItemsTableFilterComposer f) f) { + final $$TodoItemsTableFilterComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.id, - referencedTable: $state.db.todoItems, + referencedTable: $db.todoItems, getReferencedColumn: (t) => t.listId, - builder: (joinBuilder, parentComposers) => - $$TodoItemsTableFilterComposer(ComposerState( - $state.db, $state.db.todoItems, joinBuilder, parentComposers))); + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$TodoItemsTableFilterComposer( + $db: $db, + $table: $db.todoItems, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); return f(composer); } } class $$ListItemsTableOrderingComposer - extends OrderingComposer<_$AppDatabase, $ListItemsTable> { - $$ListItemsTableOrderingComposer(super.$state); - ColumnOrderings get id => $state.composableBuilder( - column: $state.table.id, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get createdAt => $state.composableBuilder( - column: $state.table.createdAt, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get name => $state.composableBuilder( - column: $state.table.name, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get ownerId => $state.composableBuilder( - column: $state.table.ownerId, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); + extends Composer<_$AppDatabase, $ListItemsTable> { + $$ListItemsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get ownerId => $composableBuilder( + column: $table.ownerId, builder: (column) => ColumnOrderings(column)); +} + +class $$ListItemsTableAnnotationComposer + extends Composer<_$AppDatabase, $ListItemsTable> { + $$ListItemsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get ownerId => + $composableBuilder(column: $table.ownerId, builder: (column) => column); + + Expression todoItemsRefs( + Expression Function($$TodoItemsTableAnnotationComposer a) f) { + final $$TodoItemsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.todoItems, + getReferencedColumn: (t) => t.listId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$TodoItemsTableAnnotationComposer( + $db: $db, + $table: $db.todoItems, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } } class $$ListItemsTableTableManager extends RootTableManager< @@ -889,6 +936,7 @@ class $$ListItemsTableTableManager extends RootTableManager< ListItem, $$ListItemsTableFilterComposer, $$ListItemsTableOrderingComposer, + $$ListItemsTableAnnotationComposer, $$ListItemsTableCreateCompanionBuilder, $$ListItemsTableUpdateCompanionBuilder, (ListItem, $$ListItemsTableReferences), @@ -898,10 +946,12 @@ class $$ListItemsTableTableManager extends RootTableManager< : super(TableManagerState( db: db, table: table, - filteringComposer: - $$ListItemsTableFilterComposer(ComposerState(db, table)), - orderingComposer: - $$ListItemsTableOrderingComposer(ComposerState(db, table)), + createFilteringComposer: () => + $$ListItemsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ListItemsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ListItemsTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value id = const Value.absent(), Value createdAt = const Value.absent(), @@ -968,6 +1018,7 @@ typedef $$ListItemsTableProcessedTableManager = ProcessedTableManager< ListItem, $$ListItemsTableFilterComposer, $$ListItemsTableOrderingComposer, + $$ListItemsTableAnnotationComposer, $$ListItemsTableCreateCompanionBuilder, $$ListItemsTableUpdateCompanionBuilder, (ListItem, $$ListItemsTableReferences), @@ -1005,10 +1056,11 @@ final class $$TodoItemsTableReferences static $ListItemsTable _listIdTable(_$AppDatabase db) => db.listItems .createAlias($_aliasNameGenerator(db.todoItems.listId, db.listItems.id)); - $$ListItemsTableProcessedTableManager? get listId { - if ($_item.listId == null) return null; + $$ListItemsTableProcessedTableManager get listId { + final $_column = $_itemColumn('list_id')!; + final manager = $$ListItemsTableTableManager($_db, $_db.listItems) - .filter((f) => f.id($_item.listId!)); + .filter((f) => f.id.sqlEquals($_column)); final item = $_typedResult.readTableOrNull(_listIdTable($_db)); if (item == null) return manager; return ProcessedTableManager( @@ -1017,113 +1069,163 @@ final class $$TodoItemsTableReferences } class $$TodoItemsTableFilterComposer - extends FilterComposer<_$AppDatabase, $TodoItemsTable> { - $$TodoItemsTableFilterComposer(super.$state); - ColumnFilters get id => $state.composableBuilder( - column: $state.table.id, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); - - ColumnFilters get photoId => $state.composableBuilder( - column: $state.table.photoId, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); - - ColumnFilters get createdAt => $state.composableBuilder( - column: $state.table.createdAt, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); - - ColumnFilters get completedAt => $state.composableBuilder( - column: $state.table.completedAt, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); - - ColumnFilters get completed => $state.composableBuilder( - column: $state.table.completed, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); - - ColumnFilters get description => $state.composableBuilder( - column: $state.table.description, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); - - ColumnFilters get createdBy => $state.composableBuilder( - column: $state.table.createdBy, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); - - ColumnFilters get completedBy => $state.composableBuilder( - column: $state.table.completedBy, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); + extends Composer<_$AppDatabase, $TodoItemsTable> { + $$TodoItemsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); + + ColumnFilters get photoId => $composableBuilder( + column: $table.photoId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get completedAt => $composableBuilder( + column: $table.completedAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get completed => $composableBuilder( + column: $table.completed, builder: (column) => ColumnFilters(column)); + + ColumnFilters get description => $composableBuilder( + column: $table.description, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdBy => $composableBuilder( + column: $table.createdBy, builder: (column) => ColumnFilters(column)); + + ColumnFilters get completedBy => $composableBuilder( + column: $table.completedBy, builder: (column) => ColumnFilters(column)); $$ListItemsTableFilterComposer get listId { - final $$ListItemsTableFilterComposer composer = $state.composerBuilder( + final $$ListItemsTableFilterComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.listId, - referencedTable: $state.db.listItems, + referencedTable: $db.listItems, getReferencedColumn: (t) => t.id, - builder: (joinBuilder, parentComposers) => - $$ListItemsTableFilterComposer(ComposerState( - $state.db, $state.db.listItems, joinBuilder, parentComposers))); + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ListItemsTableFilterComposer( + $db: $db, + $table: $db.listItems, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); return composer; } } class $$TodoItemsTableOrderingComposer - extends OrderingComposer<_$AppDatabase, $TodoItemsTable> { - $$TodoItemsTableOrderingComposer(super.$state); - ColumnOrderings get id => $state.composableBuilder( - column: $state.table.id, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get photoId => $state.composableBuilder( - column: $state.table.photoId, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get createdAt => $state.composableBuilder( - column: $state.table.createdAt, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get completedAt => $state.composableBuilder( - column: $state.table.completedAt, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get completed => $state.composableBuilder( - column: $state.table.completed, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get description => $state.composableBuilder( - column: $state.table.description, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get createdBy => $state.composableBuilder( - column: $state.table.createdBy, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get completedBy => $state.composableBuilder( - column: $state.table.completedBy, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); + extends Composer<_$AppDatabase, $TodoItemsTable> { + $$TodoItemsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get photoId => $composableBuilder( + column: $table.photoId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get completedAt => $composableBuilder( + column: $table.completedAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get completed => $composableBuilder( + column: $table.completed, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get description => $composableBuilder( + column: $table.description, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdBy => $composableBuilder( + column: $table.createdBy, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get completedBy => $composableBuilder( + column: $table.completedBy, builder: (column) => ColumnOrderings(column)); $$ListItemsTableOrderingComposer get listId { - final $$ListItemsTableOrderingComposer composer = $state.composerBuilder( + final $$ListItemsTableOrderingComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.listId, - referencedTable: $state.db.listItems, + referencedTable: $db.listItems, getReferencedColumn: (t) => t.id, - builder: (joinBuilder, parentComposers) => - $$ListItemsTableOrderingComposer(ComposerState( - $state.db, $state.db.listItems, joinBuilder, parentComposers))); + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ListItemsTableOrderingComposer( + $db: $db, + $table: $db.listItems, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$TodoItemsTableAnnotationComposer + extends Composer<_$AppDatabase, $TodoItemsTable> { + $$TodoItemsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get photoId => + $composableBuilder(column: $table.photoId, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get completedAt => $composableBuilder( + column: $table.completedAt, builder: (column) => column); + + GeneratedColumn get completed => + $composableBuilder(column: $table.completed, builder: (column) => column); + + GeneratedColumn get description => $composableBuilder( + column: $table.description, builder: (column) => column); + + GeneratedColumn get createdBy => + $composableBuilder(column: $table.createdBy, builder: (column) => column); + + GeneratedColumn get completedBy => $composableBuilder( + column: $table.completedBy, builder: (column) => column); + + $$ListItemsTableAnnotationComposer get listId { + final $$ListItemsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.listId, + referencedTable: $db.listItems, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ListItemsTableAnnotationComposer( + $db: $db, + $table: $db.listItems, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); return composer; } } @@ -1134,6 +1236,7 @@ class $$TodoItemsTableTableManager extends RootTableManager< TodoItem, $$TodoItemsTableFilterComposer, $$TodoItemsTableOrderingComposer, + $$TodoItemsTableAnnotationComposer, $$TodoItemsTableCreateCompanionBuilder, $$TodoItemsTableUpdateCompanionBuilder, (TodoItem, $$TodoItemsTableReferences), @@ -1143,10 +1246,12 @@ class $$TodoItemsTableTableManager extends RootTableManager< : super(TableManagerState( db: db, table: table, - filteringComposer: - $$TodoItemsTableFilterComposer(ComposerState(db, table)), - orderingComposer: - $$TodoItemsTableOrderingComposer(ComposerState(db, table)), + createFilteringComposer: () => + $$TodoItemsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$TodoItemsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$TodoItemsTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value id = const Value.absent(), Value listId = const Value.absent(), @@ -1216,6 +1321,7 @@ class $$TodoItemsTableTableManager extends RootTableManager< dynamic, dynamic, dynamic, + dynamic, dynamic>>(state) { if (listId) { state = state.withJoin( @@ -1244,6 +1350,7 @@ typedef $$TodoItemsTableProcessedTableManager = ProcessedTableManager< TodoItem, $$TodoItemsTableFilterComposer, $$TodoItemsTableOrderingComposer, + $$TodoItemsTableAnnotationComposer, $$TodoItemsTableCreateCompanionBuilder, $$TodoItemsTableUpdateCompanionBuilder, (TodoItem, $$TodoItemsTableReferences), diff --git a/demos/supabase-todolist-drift/lib/powersync/fts5.dart b/demos/supabase-todolist-drift/lib/powersync/fts5.dart new file mode 100644 index 00000000..85409035 --- /dev/null +++ b/demos/supabase-todolist-drift/lib/powersync/fts5.dart @@ -0,0 +1,89 @@ +import 'dart:async'; + +import 'package:drift/drift.dart'; + +import 'schema.dart'; + +Future createFts5Tables({ + required DatabaseConnectionUser db, + required String tableName, + required List columns, + String tokenizationMethod = 'unicode61', +}) async { + String internalName = + schema.tables.firstWhere((table) => table.name == tableName).internalName; + String stringColumns = columns.join(', '); + + await db.customStatement(''' + CREATE VIRTUAL TABLE IF NOT EXISTS fts_$tableName + USING fts5(id UNINDEXED, $stringColumns, tokenize='$tokenizationMethod'); + '''); + // Copy over records already in table + await db.customStatement(''' + INSERT INTO fts_$tableName(rowid, id, $stringColumns) + SELECT rowid, id, ${generateJsonExtracts(ExtractType.columnOnly, 'data', columns)} FROM $internalName; + '''); + // Add INSERT, UPDATE and DELETE and triggers to keep fts table in sync with table + await db.customStatement(''' + CREATE TRIGGER IF NOT EXISTS fts_insert_trigger_$tableName AFTER INSERT ON $internalName + BEGIN + INSERT INTO fts_$tableName(rowid, id, $stringColumns) + VALUES ( + NEW.rowid, + NEW.id, + ${generateJsonExtracts(ExtractType.columnOnly, 'NEW.data', columns)} + ); + END; + '''); + await db.customStatement(''' + CREATE TRIGGER IF NOT EXISTS fts_update_trigger_$tableName AFTER UPDATE ON $internalName BEGIN + UPDATE fts_$tableName + SET ${generateJsonExtracts(ExtractType.columnInOperation, 'NEW.data', columns)} + WHERE rowid = NEW.rowid; + END; + '''); + await db.customStatement(''' + CREATE TRIGGER IF NOT EXISTS fts_delete_trigger_$tableName AFTER DELETE ON $internalName BEGIN + DELETE FROM fts_$tableName WHERE rowid = OLD.rowid; + END; + '''); +} + +typedef ExtractGenerator = String Function(String, String); + +enum ExtractType { + columnOnly, + columnInOperation, +} + +typedef ExtractGeneratorMap = Map; + +String _createExtract(String jsonColumnName, String columnName) => + 'json_extract($jsonColumnName, \'\$.$columnName\')'; + +ExtractGeneratorMap extractGeneratorsMap = { + ExtractType.columnOnly: ( + String jsonColumnName, + String columnName, + ) => + _createExtract(jsonColumnName, columnName), + ExtractType.columnInOperation: ( + String jsonColumnName, + String columnName, + ) => + '$columnName = ${_createExtract(jsonColumnName, columnName)}', +}; + +String generateJsonExtracts( + ExtractType type, String jsonColumnName, List columns) { + ExtractGenerator? generator = extractGeneratorsMap[type]; + if (generator == null) { + throw StateError('Unexpected null generator for key: $type'); + } + + if (columns.length == 1) { + return generator(jsonColumnName, columns.first); + } + + return columns.map((column) => generator(jsonColumnName, column)).join(', '); +} diff --git a/demos/supabase-todolist-drift/lib/fts_helpers.dart b/demos/supabase-todolist-drift/lib/powersync/fts_helpers.dart similarity index 66% rename from demos/supabase-todolist-drift/lib/fts_helpers.dart rename to demos/supabase-todolist-drift/lib/powersync/fts_helpers.dart index 5e5ad5c3..f9e898a0 100644 --- a/demos/supabase-todolist-drift/lib/fts_helpers.dart +++ b/demos/supabase-todolist-drift/lib/powersync/fts_helpers.dart @@ -1,4 +1,9 @@ -import 'package:supabase_todolist_drift/powersync.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import 'powersync.dart'; + +part 'fts_helpers.g.dart'; String _createSearchTermWithOptions(String searchTerm) { // adding * to the end of the search term will match any word that starts with the search term @@ -9,8 +14,10 @@ String _createSearchTermWithOptions(String searchTerm) { } /// Search the FTS table for the given searchTerm -Future search(String searchTerm, String tableName) async { +@riverpod +Future search(Ref ref, String searchTerm, String tableName) async { String searchTermWithOptions = _createSearchTermWithOptions(searchTerm); + final db = await ref.read(powerSyncInstanceProvider.future); return await db.getAll( 'SELECT * FROM fts_$tableName WHERE fts_$tableName MATCH ? ORDER BY rank', [searchTermWithOptions]); diff --git a/demos/supabase-todolist-drift/lib/powersync/fts_helpers.g.dart b/demos/supabase-todolist-drift/lib/powersync/fts_helpers.g.dart new file mode 100644 index 00000000..8c0efb6f --- /dev/null +++ b/demos/supabase-todolist-drift/lib/powersync/fts_helpers.g.dart @@ -0,0 +1,188 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'fts_helpers.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$searchHash() => r'44beab2ea36342be88731c46c2988e76058e7fe2'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// Search the FTS table for the given searchTerm +/// +/// Copied from [search]. +@ProviderFor(search) +const searchProvider = SearchFamily(); + +/// Search the FTS table for the given searchTerm +/// +/// Copied from [search]. +class SearchFamily extends Family> { + /// Search the FTS table for the given searchTerm + /// + /// Copied from [search]. + const SearchFamily(); + + /// Search the FTS table for the given searchTerm + /// + /// Copied from [search]. + SearchProvider call( + String searchTerm, + String tableName, + ) { + return SearchProvider( + searchTerm, + tableName, + ); + } + + @override + SearchProvider getProviderOverride( + covariant SearchProvider provider, + ) { + return call( + provider.searchTerm, + provider.tableName, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'searchProvider'; +} + +/// Search the FTS table for the given searchTerm +/// +/// Copied from [search]. +class SearchProvider extends AutoDisposeFutureProvider { + /// Search the FTS table for the given searchTerm + /// + /// Copied from [search]. + SearchProvider( + String searchTerm, + String tableName, + ) : this._internal( + (ref) => search( + ref as SearchRef, + searchTerm, + tableName, + ), + from: searchProvider, + name: r'searchProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$searchHash, + dependencies: SearchFamily._dependencies, + allTransitiveDependencies: SearchFamily._allTransitiveDependencies, + searchTerm: searchTerm, + tableName: tableName, + ); + + SearchProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.searchTerm, + required this.tableName, + }) : super.internal(); + + final String searchTerm; + final String tableName; + + @override + Override overrideWith( + FutureOr Function(SearchRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: SearchProvider._internal( + (ref) => create(ref as SearchRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + searchTerm: searchTerm, + tableName: tableName, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _SearchProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is SearchProvider && + other.searchTerm == searchTerm && + other.tableName == tableName; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, searchTerm.hashCode); + hash = _SystemHash.combine(hash, tableName.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin SearchRef on AutoDisposeFutureProviderRef { + /// The parameter `searchTerm` of this provider. + String get searchTerm; + + /// The parameter `tableName` of this provider. + String get tableName; +} + +class _SearchProviderElement extends AutoDisposeFutureProviderElement + with SearchRef { + _SearchProviderElement(super.provider); + + @override + String get searchTerm => (origin as SearchProvider).searchTerm; + @override + String get tableName => (origin as SearchProvider).tableName; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/demos/supabase-todolist-drift/lib/powersync/powersync.dart b/demos/supabase-todolist-drift/lib/powersync/powersync.dart new file mode 100644 index 00000000..cb327bfd --- /dev/null +++ b/demos/supabase-todolist-drift/lib/powersync/powersync.dart @@ -0,0 +1,79 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:powersync/powersync.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:stream_transform/stream_transform.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +import '../supabase.dart'; +import 'connector.dart'; +import 'schema.dart'; + +part 'powersync.g.dart'; + +@Riverpod(keepAlive: true) +Future powerSyncInstance(Ref ref) async { + final db = PowerSyncDatabase( + schema: schema, + path: await _getDatabasePath(), + logger: attachedLogger, + ); + await db.initialize(); + + SupabaseConnector? currentConnector; + if (ref.read(sessionProvider).value != null) { + currentConnector = SupabaseConnector(); + db.connect(connector: currentConnector); + } + + final instance = Supabase.instance.client.auth; + final sub = instance.onAuthStateChange.listen((data) async { + final event = data.event; + if (event == AuthChangeEvent.signedIn) { + currentConnector = SupabaseConnector(); + db.connect(connector: currentConnector!); + } else if (event == AuthChangeEvent.signedOut) { + currentConnector = null; + await db.disconnect(); + } else if (event == AuthChangeEvent.tokenRefreshed) { + currentConnector?.prefetchCredentials(); + } + }); + ref.onDispose(sub.cancel); + ref.onDispose(db.close); + + return db; +} + +final _syncStatusInternal = StreamProvider((ref) { + return Stream.fromFuture( + ref.watch(powerSyncInstanceProvider.future), + ).asyncExpand((db) => db.statusStream).startWith(null); +}); + +final syncStatus = Provider((ref) { + // ignore: invalid_use_of_internal_member + return ref.watch(_syncStatusInternal).value ?? const SyncStatus(); +}); + +@riverpod +bool didCompleteSync(Ref ref, [StreamPriority? priority]) { + final status = ref.watch(syncStatus); + if (priority != null) { + return status.statusForPriority(priority).hasSynced ?? false; + } else { + return status.hasSynced ?? false; + } +} + +Future _getDatabasePath() async { + const dbFilename = 'powersync-demo.db'; + // getApplicationSupportDirectory is not supported on Web + if (kIsWeb) { + return dbFilename; + } + final dir = await getApplicationSupportDirectory(); + return join(dir.path, dbFilename); +} diff --git a/demos/supabase-todolist-drift/lib/powersync/powersync.g.dart b/demos/supabase-todolist-drift/lib/powersync/powersync.g.dart new file mode 100644 index 00000000..8a0f6344 --- /dev/null +++ b/demos/supabase-todolist-drift/lib/powersync/powersync.g.dart @@ -0,0 +1,177 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'powersync.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$powerSyncInstanceHash() => r'd4ccd204e3e5b32f7e6111601de19179cbdd9f41'; + +/// See also [powerSyncInstance]. +@ProviderFor(powerSyncInstance) +final powerSyncInstanceProvider = FutureProvider.internal( + powerSyncInstance, + name: r'powerSyncInstanceProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$powerSyncInstanceHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef PowerSyncInstanceRef = FutureProviderRef; +String _$didCompleteSyncHash() => r'532f9cd620c43578b58452907e2165eba6745c21'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [didCompleteSync]. +@ProviderFor(didCompleteSync) +const didCompleteSyncProvider = DidCompleteSyncFamily(); + +/// See also [didCompleteSync]. +class DidCompleteSyncFamily extends Family { + /// See also [didCompleteSync]. + const DidCompleteSyncFamily(); + + /// See also [didCompleteSync]. + DidCompleteSyncProvider call([ + BucketPriority? priority, + ]) { + return DidCompleteSyncProvider( + priority, + ); + } + + @override + DidCompleteSyncProvider getProviderOverride( + covariant DidCompleteSyncProvider provider, + ) { + return call( + provider.priority, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'didCompleteSyncProvider'; +} + +/// See also [didCompleteSync]. +class DidCompleteSyncProvider extends AutoDisposeProvider { + /// See also [didCompleteSync]. + DidCompleteSyncProvider([ + BucketPriority? priority, + ]) : this._internal( + (ref) => didCompleteSync( + ref as DidCompleteSyncRef, + priority, + ), + from: didCompleteSyncProvider, + name: r'didCompleteSyncProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$didCompleteSyncHash, + dependencies: DidCompleteSyncFamily._dependencies, + allTransitiveDependencies: + DidCompleteSyncFamily._allTransitiveDependencies, + priority: priority, + ); + + DidCompleteSyncProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.priority, + }) : super.internal(); + + final BucketPriority? priority; + + @override + Override overrideWith( + bool Function(DidCompleteSyncRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: DidCompleteSyncProvider._internal( + (ref) => create(ref as DidCompleteSyncRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + priority: priority, + ), + ); + } + + @override + AutoDisposeProviderElement createElement() { + return _DidCompleteSyncProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is DidCompleteSyncProvider && other.priority == priority; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, priority.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin DidCompleteSyncRef on AutoDisposeProviderRef { + /// The parameter `priority` of this provider. + BucketPriority? get priority; +} + +class _DidCompleteSyncProviderElement extends AutoDisposeProviderElement + with DidCompleteSyncRef { + _DidCompleteSyncProviderElement(super.provider); + + @override + BucketPriority? get priority => (origin as DidCompleteSyncProvider).priority; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/demos/supabase-todolist-drift/lib/queries.drift b/demos/supabase-todolist-drift/lib/powersync/queries.drift similarity index 100% rename from demos/supabase-todolist-drift/lib/queries.drift rename to demos/supabase-todolist-drift/lib/powersync/queries.drift diff --git a/demos/supabase-todolist-drift/lib/models/schema.dart b/demos/supabase-todolist-drift/lib/powersync/schema.dart similarity index 100% rename from demos/supabase-todolist-drift/lib/models/schema.dart rename to demos/supabase-todolist-drift/lib/powersync/schema.dart diff --git a/demos/supabase-todolist-drift/lib/screens/add_item_dialog.dart b/demos/supabase-todolist-drift/lib/screens/add_item_dialog.dart new file mode 100644 index 00000000..2e3c24af --- /dev/null +++ b/demos/supabase-todolist-drift/lib/screens/add_item_dialog.dart @@ -0,0 +1,52 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../stores/items.dart'; + +@RoutePage(name: 'AddItemRoute') +final class AddItemDialog extends HookConsumerWidget { + final String list; + + const AddItemDialog({super.key, required this.list}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final controller = useTextEditingController(); + + Future add() async { + await ref + .read(itemsNotifierProvider(list).notifier) + .addItem(controller.text); + if (context.mounted) { + context.pop(); + } + } + + return AlertDialog( + title: const Text('Add a new todo item'), + content: TextField( + controller: controller, + decoration: const InputDecoration(hintText: 'Type your new todo'), + onSubmitted: (value) { + add(); + }, + autofocus: true, + ), + actions: [ + OutlinedButton( + child: const Text('Cancel'), + onPressed: () { + controller.clear(); + Navigator.of(context).pop(); + }, + ), + ElevatedButton( + onPressed: add, + child: const Text('Add'), + ), + ], + ); + } +} diff --git a/demos/supabase-todolist-drift/lib/screens/add_list_dialog.dart b/demos/supabase-todolist-drift/lib/screens/add_list_dialog.dart new file mode 100644 index 00000000..b0e35647 --- /dev/null +++ b/demos/supabase-todolist-drift/lib/screens/add_list_dialog.dart @@ -0,0 +1,52 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../stores/lists.dart'; + +@RoutePage(name: 'AddListRoute') +final class AddListDialog extends HookConsumerWidget { + const AddListDialog({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final textController = useTextEditingController(); + + Future add() async { + await ref + .read(listsNotifierProvider.notifier) + .createNewList(textController.text); + if (context.mounted) { + context.pop(); + } + } + + return AlertDialog( + title: const Text('Add a new list'), + content: TextField( + controller: textController, + decoration: const InputDecoration(hintText: 'List name'), + onSubmitted: (value) async { + await add(); + }, + autofocus: true, + ), + actions: [ + OutlinedButton( + child: const Text('Cancel'), + onPressed: () { + textController.clear(); + context.pop(); + }, + ), + ElevatedButton( + child: const Text('Create'), + onPressed: () async { + await add(); + }, + ), + ], + ); + } +} diff --git a/demos/supabase-todolist-drift/lib/screens/list_details.dart b/demos/supabase-todolist-drift/lib/screens/list_details.dart new file mode 100644 index 00000000..fbde5109 --- /dev/null +++ b/demos/supabase-todolist-drift/lib/screens/list_details.dart @@ -0,0 +1,116 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../app_config.dart'; +import '../components/page_layout.dart'; +import '../components/photo_widget.dart'; +import '../navigation.dart'; +import '../powersync/database.dart'; +import '../stores/items.dart'; + +@RoutePage() +final class ListsDetailsPage extends ConsumerWidget { + final String list; + + const ListsDetailsPage({super.key, required this.list}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return PageLayout( + title: const Text('Todo List'), + showDrawer: false, + content: _ItemsInListWidget(list: list), + floatingActionButton: FloatingActionButton( + onPressed: () { + context.pushRoute(AddItemRoute(list: list)); + }, + tooltip: 'Add new item', + child: const Icon(Icons.add), + ), + ); + } +} + +final class _ItemsInListWidget extends ConsumerWidget { + final String list; + + const _ItemsInListWidget({required this.list}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final items = ref.watch(itemsNotifierProvider(list)); + + return items.maybeWhen( + data: (items) => ListView( + padding: const EdgeInsets.symmetric(vertical: 8.0), + children: items.map((todo) { + return _TodoItemWidget( + todo: todo, + ); + }).toList(), + ), + orElse: () => const CircularProgressIndicator(), + ); + } +} + +final class _TodoItemWidget extends ConsumerWidget { + _TodoItemWidget({ + required this.todo, + }) : super(key: ObjectKey(todo.id)); + + final TodoItem todo; + + TextStyle? _getTextStyle(bool checked) { + if (!checked) return null; + + return const TextStyle( + color: Colors.black54, + decoration: TextDecoration.lineThrough, + ); + } + + Future deleteTodo(WidgetRef ref) async { + await ref + .read(itemsNotifierProvider(todo.listId).notifier) + .deleteItem(todo); + } + + Future toggleTodo(WidgetRef ref) async { + await ref + .read(itemsNotifierProvider(todo.listId).notifier) + .toggleTodo(todo); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ListTile( + onTap: () => toggleTodo(ref), + leading: Checkbox( + value: todo.completed, + onChanged: (_) => toggleTodo(ref), + ), + title: Row( + children: [ + Expanded( + child: Text(todo.description, + style: _getTextStyle(todo.completed == true))), + IconButton( + iconSize: 30, + icon: const Icon( + Icons.delete, + color: Colors.red, + ), + alignment: Alignment.centerRight, + onPressed: () async => await deleteTodo(ref), + tooltip: 'Delete Item', + ), + AppConfig.supabaseStorageBucket.isEmpty + ? Container() + : PhotoWidget(todo: todo), + ], + ), + ); + } +} diff --git a/demos/supabase-todolist-drift/lib/screens/lists.dart b/demos/supabase-todolist-drift/lib/screens/lists.dart new file mode 100644 index 00000000..c995a275 --- /dev/null +++ b/demos/supabase-todolist-drift/lib/screens/lists.dart @@ -0,0 +1,108 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:powersync/powersync.dart' hide Column; + +import '../components/page_layout.dart'; +import '../navigation.dart'; +import '../powersync/database.dart'; +import '../powersync/powersync.dart'; +import '../stores/lists.dart'; + +@RoutePage() +final class ListsPage extends ConsumerWidget { + const ListsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return PageLayout( + title: const Text('Todo Lists'), + content: const _ListsWidget(), + floatingActionButton: FloatingActionButton( + onPressed: () { + context.pushRoute(const AddListRoute()); + }, + tooltip: 'Create List', + child: const Icon(Icons.add), + ), + ); + } +} + +final class _ListsWidget extends ConsumerWidget { + const _ListsWidget(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final lists = ref.watch(listsNotifierProvider); + final didSync = ref.watch(didCompleteSyncProvider(StreamPriority(1))); + + if (!didSync) { + return const Text('Busy with sync...'); + } + + return lists.map( + data: (data) { + return ListView( + padding: const EdgeInsets.symmetric(vertical: 8.0), + children: data.value.map((list) { + return ListItemWidget(list: list); + }).toList(), + ); + }, + error: (_) => const Text('Error loading lists'), + loading: (_) => const CircularProgressIndicator(), + ); + } +} + +class ListItemWidget extends ConsumerWidget { + ListItemWidget({ + required this.list, + }) : super(key: ObjectKey(list)); + + final ListItemWithStats list; + + @override + Widget build(BuildContext context, WidgetRef ref) { + Future delete() async { + await ref.read(listsNotifierProvider.notifier).deleteList(list.self.id); + } + + void viewList() { + context.pushRoute(ListsDetailsRoute(list: list.self.id)); + } + + final subtext = + '${list.pendingCount} pending, ${list.completedCount} completed'; + + return Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + onTap: viewList, + leading: const Icon(Icons.list), + title: Text(list.self.name), + subtitle: Text(subtext)), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton( + iconSize: 30, + icon: const Icon( + Icons.delete, + color: Colors.red, + ), + tooltip: 'Delete List', + alignment: Alignment.centerRight, + onPressed: delete, + ), + const SizedBox(width: 8), + ], + ), + ], + ), + ); + } +} diff --git a/demos/supabase-todolist-drift/lib/screens/login.dart b/demos/supabase-todolist-drift/lib/screens/login.dart new file mode 100644 index 00000000..15eea101 --- /dev/null +++ b/demos/supabase-todolist-drift/lib/screens/login.dart @@ -0,0 +1,84 @@ +import 'package:auto_route/annotations.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../components/app_bar.dart'; +import '../navigation.dart'; +import '../supabase.dart'; + +@RoutePage() +final class LoginPage extends HookConsumerWidget { + const LoginPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final usernameController = useTextEditingController(); + final passwordController = useTextEditingController(); + final (:error, :isBusy) = ref.watch(authNotifierProvider); + + final loginAction = isBusy + ? null + : () { + ref + .read(authNotifierProvider.notifier) + .login(usernameController.text, passwordController.text); + }; + + return Scaffold( + appBar: appBar, + body: SingleChildScrollView( + child: Container( + margin: const EdgeInsets.all(30), + alignment: Alignment.center, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 300), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Padding( + padding: EdgeInsets.only(bottom: 15), + child: Text('Supabase Signup'), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: TextFormField( + controller: usernameController, + decoration: const InputDecoration(labelText: "Email"), + enabled: !isBusy, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: TextFormField( + obscureText: true, + controller: passwordController, + decoration: InputDecoration( + labelText: "Password", errorText: error), + enabled: !isBusy, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 15), + child: TextButton( + onPressed: loginAction, + child: const Text('Login'), + ), + ), + TextButton( + onPressed: isBusy + ? null + : () { + ref.read(appRouter).replace(const SignupRoute()); + }, + child: const Text('Sign Up'), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/demos/supabase-todolist-drift/lib/screens/search.dart b/demos/supabase-todolist-drift/lib/screens/search.dart new file mode 100644 index 00000000..dce33806 --- /dev/null +++ b/demos/supabase-todolist-drift/lib/screens/search.dart @@ -0,0 +1,120 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:logging/logging.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../navigation.dart'; +import '../powersync/database.dart'; +import '../powersync/fts_helpers.dart' as fts_helpers; + +part 'search.g.dart'; + +final log = Logger('powersync-supabase'); + +class FtsSearchDelegate extends SearchDelegate { + @override + List? buildActions(BuildContext context) { + return [ + IconButton( + onPressed: () { + query = ''; + }, + icon: const Icon(Icons.clear), + ), + ]; + } + + @override + Widget? buildLeading(BuildContext context) { + return IconButton( + onPressed: () { + close(context, null); + }, + icon: const Icon(Icons.arrow_back), + ); + } + + @override + Widget buildResults(BuildContext context) { + return Consumer(builder: (context, ref, _) { + final results = ref.watch(_searchProvider(query)); + + return results.maybeWhen( + data: (rows) { + return ListView.builder( + itemBuilder: (context, index) { + return ListTile( + title: Text(rows[index]['name']), + onTap: () { + close(context, null); + }, + ); + }, + itemCount: rows.length, + ); + }, + orElse: () => const Center( + child: CircularProgressIndicator(), + ), + ); + }); + } + + @override + Widget buildSuggestions(BuildContext context) { + return Consumer( + builder: (context, ref, _) { + final results = ref.watch(_searchProvider(query)); + final appDb = ref.watch(driftDatabase); + + return results.maybeWhen( + data: (rows) { + return ListView.builder( + itemBuilder: (context, index) { + return ListTile( + title: Text(rows[index]['name'] ?? ''), + onTap: () async { + ListItem list = await appDb.findList(rows[index]['id']); + if (context.mounted) { + context.pushRoute(ListsDetailsRoute(list: list.id)); + } + }, + ); + }, + itemCount: rows.length, + ); + }, + orElse: () => const Center( + child: CircularProgressIndicator(), + ), + ); + }, + ); + } +} + +@riverpod +Future _search(Ref ref, String query) async { + if (query.isEmpty) return []; + final listsSearchResults = + await ref.watch(fts_helpers.searchProvider(query, 'lists').future); + final todoItemsSearchResults = + await ref.watch(fts_helpers.searchProvider(query, 'todos').future); + + List formattedListResults = listsSearchResults + .map((result) => {"id": result['id'], "name": result['name']}) + .toList(); + List formattedTodoItemsResults = todoItemsSearchResults + .map((result) => { + // Use list_id so the navigation goes to the list page + "id": result['list_id'], + "name": result['description'], + }) + .toList(); + List formattedResults = [ + ...formattedListResults, + ...formattedTodoItemsResults + ]; + return formattedResults; +} diff --git a/demos/supabase-todolist-drift/lib/screens/search.g.dart b/demos/supabase-todolist-drift/lib/screens/search.g.dart new file mode 100644 index 00000000..48426f71 --- /dev/null +++ b/demos/supabase-todolist-drift/lib/screens/search.g.dart @@ -0,0 +1,159 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'search.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$searchHash() => r'22f755afc645f10c862d9aece9f392958c10d086'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [_search]. +@ProviderFor(_search) +const _searchProvider = _SearchFamily(); + +/// See also [_search]. +class _SearchFamily extends Family> { + /// See also [_search]. + const _SearchFamily(); + + /// See also [_search]. + _SearchProvider call( + String query, + ) { + return _SearchProvider( + query, + ); + } + + @override + _SearchProvider getProviderOverride( + covariant _SearchProvider provider, + ) { + return call( + provider.query, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'_searchProvider'; +} + +/// See also [_search]. +class _SearchProvider extends AutoDisposeFutureProvider { + /// See also [_search]. + _SearchProvider( + String query, + ) : this._internal( + (ref) => _search( + ref as _SearchRef, + query, + ), + from: _searchProvider, + name: r'_searchProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$searchHash, + dependencies: _SearchFamily._dependencies, + allTransitiveDependencies: _SearchFamily._allTransitiveDependencies, + query: query, + ); + + _SearchProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.query, + }) : super.internal(); + + final String query; + + @override + Override overrideWith( + FutureOr Function(_SearchRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: _SearchProvider._internal( + (ref) => create(ref as _SearchRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + query: query, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _SearchProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is _SearchProvider && other.query == query; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, query.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin _SearchRef on AutoDisposeFutureProviderRef { + /// The parameter `query` of this provider. + String get query; +} + +class _SearchProviderElement extends AutoDisposeFutureProviderElement + with _SearchRef { + _SearchProviderElement(super.provider); + + @override + String get query => (origin as _SearchProvider).query; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/demos/supabase-todolist-drift/lib/screens/signup.dart b/demos/supabase-todolist-drift/lib/screens/signup.dart new file mode 100644 index 00000000..6b22271e --- /dev/null +++ b/demos/supabase-todolist-drift/lib/screens/signup.dart @@ -0,0 +1,84 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../components/app_bar.dart'; +import '../navigation.dart'; +import '../supabase.dart'; + +@RoutePage() +final class SignupPage extends HookConsumerWidget { + const SignupPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final usernameController = useTextEditingController(); + final passwordController = useTextEditingController(); + final (:error, :isBusy) = ref.watch(authNotifierProvider); + + final signupAction = isBusy + ? null + : () { + ref + .read(authNotifierProvider.notifier) + .signup(usernameController.text, passwordController.text); + }; + + return Scaffold( + appBar: appBar, + body: SingleChildScrollView( + child: Container( + margin: const EdgeInsets.all(30), + alignment: Alignment.center, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 300), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Padding( + padding: EdgeInsets.only(bottom: 15), + child: Text('Supabase Login'), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: TextFormField( + controller: usernameController, + decoration: const InputDecoration(labelText: 'Email'), + enabled: !isBusy, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: TextFormField( + obscureText: true, + controller: passwordController, + decoration: InputDecoration( + labelText: 'Password', errorText: error), + enabled: !isBusy, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 15), + child: TextButton( + onPressed: signupAction, + child: const Text('Sign up'), + ), + ), + TextButton( + onPressed: isBusy + ? null + : () { + ref.read(appRouter).replace(const LoginRoute()); + }, + child: const Text('Already have an account?'), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/demos/supabase-todolist-drift/lib/screens/sql_console.dart b/demos/supabase-todolist-drift/lib/screens/sql_console.dart new file mode 100644 index 00000000..0608dd0d --- /dev/null +++ b/demos/supabase-todolist-drift/lib/screens/sql_console.dart @@ -0,0 +1,99 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:powersync/sqlite3_common.dart' as sqlite; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../components/page_layout.dart'; +import '../powersync/powersync.dart'; +import '../powersync/schema.dart'; + +part 'sql_console.g.dart'; + +@riverpod +Stream _watch(Ref ref, String sql) async* { + final db = await ref.read(powerSyncInstanceProvider.future); + yield* db.watch(sql); +} + +@RoutePage() +final class SqlConsolePage extends HookConsumerWidget { + const SqlConsolePage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final query = useState('SELECT * FROM $todosTable'); + final controller = useTextEditingController(text: query.value); + final rows = ref.watch(_watchProvider(query.value)); + + return PageLayout( + showDrawer: false, + content: Column( + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: TextField( + controller: controller, + onEditingComplete: () { + query.value = controller.text; + }, + decoration: InputDecoration( + isDense: false, + border: const OutlineInputBorder(), + labelText: 'Query', + errorText: rows.error?.toString(), + ), + ), + ), + if (rows case AsyncData(:final value)) + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: ResultSetTable(data: value), + ), + )) + ], + ), + ); + } +} + +/// Stateless DataTable rendering results from a SQLite query +final class ResultSetTable extends StatelessWidget { + const ResultSetTable({super.key, this.data}); + + final sqlite.ResultSet? data; + + @override + Widget build(BuildContext context) { + if (data == null) { + return const Text('Loading...'); + } else if (data!.isEmpty) { + return const Text('Empty'); + } + return DataTable( + columns: [ + for (var column in data!.columnNames) + DataColumn( + label: Expanded( + child: Text( + column, + style: const TextStyle(fontStyle: FontStyle.italic), + ), + ), + ), + ], + rows: [ + for (var row in data!.rows) + DataRow( + cells: [ + for (var cell in row) DataCell(Text((cell ?? '').toString())), + ], + ), + ], + ); + } +} diff --git a/demos/supabase-todolist-drift/lib/screens/sql_console.g.dart b/demos/supabase-todolist-drift/lib/screens/sql_console.g.dart new file mode 100644 index 00000000..96a2949b --- /dev/null +++ b/demos/supabase-todolist-drift/lib/screens/sql_console.g.dart @@ -0,0 +1,159 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sql_console.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$watchHash() => r'd184cf5e1c494c80f42ad490e989911be7fce98a'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [_watch]. +@ProviderFor(_watch) +const _watchProvider = _WatchFamily(); + +/// See also [_watch]. +class _WatchFamily extends Family> { + /// See also [_watch]. + const _WatchFamily(); + + /// See also [_watch]. + _WatchProvider call( + String sql, + ) { + return _WatchProvider( + sql, + ); + } + + @override + _WatchProvider getProviderOverride( + covariant _WatchProvider provider, + ) { + return call( + provider.sql, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'_watchProvider'; +} + +/// See also [_watch]. +class _WatchProvider extends AutoDisposeStreamProvider { + /// See also [_watch]. + _WatchProvider( + String sql, + ) : this._internal( + (ref) => _watch( + ref as _WatchRef, + sql, + ), + from: _watchProvider, + name: r'_watchProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$watchHash, + dependencies: _WatchFamily._dependencies, + allTransitiveDependencies: _WatchFamily._allTransitiveDependencies, + sql: sql, + ); + + _WatchProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.sql, + }) : super.internal(); + + final String sql; + + @override + Override overrideWith( + Stream Function(_WatchRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: _WatchProvider._internal( + (ref) => create(ref as _WatchRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + sql: sql, + ), + ); + } + + @override + AutoDisposeStreamProviderElement createElement() { + return _WatchProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is _WatchProvider && other.sql == sql; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, sql.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin _WatchRef on AutoDisposeStreamProviderRef { + /// The parameter `sql` of this provider. + String get sql; +} + +class _WatchProviderElement + extends AutoDisposeStreamProviderElement with _WatchRef { + _WatchProviderElement(super.provider); + + @override + String get sql => (origin as _WatchProvider).sql; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/demos/supabase-todolist-drift/lib/attachments/photo_capture_widget.dart b/demos/supabase-todolist-drift/lib/screens/take_photo.dart similarity index 71% rename from demos/supabase-todolist-drift/lib/attachments/photo_capture_widget.dart rename to demos/supabase-todolist-drift/lib/screens/take_photo.dart index aa742c42..3234d579 100644 --- a/demos/supabase-todolist-drift/lib/attachments/photo_capture_widget.dart +++ b/demos/supabase-todolist-drift/lib/screens/take_photo.dart @@ -1,25 +1,31 @@ import 'dart:async'; +import 'package:auto_route/auto_route.dart'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:logging/logging.dart'; import 'package:powersync/powersync.dart' as powersync; -import 'package:supabase_todolist_drift/attachments/queue.dart'; -import 'package:supabase_todolist_drift/powersync.dart'; -class TakePhotoWidget extends StatefulWidget { +import '../powersync/attachments/queue.dart'; +import '../powersync/database.dart'; + +final _log = Logger('TakePhotoWidget'); + +@RoutePage() +class TakePhotoPage extends ConsumerStatefulWidget { final String todoId; final CameraDescription camera; - const TakePhotoWidget( - {super.key, required this.todoId, required this.camera}); + const TakePhotoPage({super.key, required this.todoId, required this.camera}); @override - State createState() { + ConsumerState createState() { return _TakePhotoWidgetState(); } } -class _TakePhotoWidgetState extends State { +class _TakePhotoWidgetState extends ConsumerState { late CameraController _cameraController; late Future _initializeControllerFuture; @@ -50,16 +56,17 @@ class _TakePhotoWidgetState extends State { final XFile photo = await _cameraController.takePicture(); // copy photo to new directory with ID as name String photoId = powersync.uuid.v4(); - String storageDirectory = await attachmentQueue.getStorageDirectory(); - await attachmentQueue.localStorage + final queue = await ref.read(attachmentQueueProvider.future); + String storageDirectory = await queue.getStorageDirectory(); + await queue.localStorage .copyFile(photo.path, '$storageDirectory/$photoId.jpg'); int photoSize = await photo.length(); - await appDb.addTodoPhoto(widget.todoId, photoId); - await attachmentQueue.saveFile(photoId, photoSize); + await ref.read(driftDatabase).addTodoPhoto(widget.todoId, photoId); + await queue.saveFile(photoId, photoSize); } catch (e) { - log.info('Error taking photo: $e'); + _log.info('Error taking photo: $e'); } // After taking the photo, navigate back to the previous screen diff --git a/demos/supabase-todolist-drift/lib/stores/items.dart b/demos/supabase-todolist-drift/lib/stores/items.dart new file mode 100644 index 00000000..888e1758 --- /dev/null +++ b/demos/supabase-todolist-drift/lib/stores/items.dart @@ -0,0 +1,62 @@ +import 'package:drift/drift.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../powersync/attachments/queue.dart'; +import '../powersync/database.dart'; +import '../supabase.dart'; + +part 'items.g.dart'; + +@riverpod +final class ItemsNotifier extends _$ItemsNotifier { + @override + Stream> build(String list) { + final database = ref.watch(driftDatabase); + final query = database.select(database.todoItems) + ..where((row) => row.listId.equals(list)) + ..orderBy([(t) => OrderingTerm(expression: t.createdAt)]); + return query.watch(); + } + + Future toggleTodo(TodoItem todo) async { + final db = ref.read(driftDatabase); + final userId = ref.read(userIdProvider); + + final stmt = db.update(db.todoItems)..where((t) => t.id.equals(todo.id)); + + if (todo.completed != true) { + await stmt.write( + TodoItemsCompanion( + completed: const Value(true), + completedAt: Value(DateTime.now()), + completedBy: Value(userId)), + ); + } else { + await stmt.write(const TodoItemsCompanion(completed: Value(false))); + } + } + + Future deleteItem(TodoItem item) async { + final db = ref.read(driftDatabase); + if (item.photoId case final photo?) { + final queue = await ref.read(attachmentQueueProvider.future); + queue.deleteFile(photo); + } + + await (db.delete(db.todoItems)..where((t) => t.id.equals(item.id))).go(); + } + + Future addItem(String description) async { + final db = ref.read(driftDatabase); + final userId = ref.read(userIdProvider); + + await db.into(db.todoItems).insertReturning( + TodoItemsCompanion.insert( + listId: list, + description: description, + completed: const Value(false), + createdBy: Value(userId), + ), + ); + } +} diff --git a/demos/supabase-todolist-drift/lib/stores/items.g.dart b/demos/supabase-todolist-drift/lib/stores/items.g.dart new file mode 100644 index 00000000..8a9c7e43 --- /dev/null +++ b/demos/supabase-todolist-drift/lib/stores/items.g.dart @@ -0,0 +1,176 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'items.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$itemsNotifierHash() => r'0cda92119ac0ce0a22bdaf05d74d17e6b1dc0f4f'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$ItemsNotifier + extends BuildlessAutoDisposeStreamNotifier> { + late final String list; + + Stream> build( + String list, + ); +} + +/// See also [ItemsNotifier]. +@ProviderFor(ItemsNotifier) +const itemsNotifierProvider = ItemsNotifierFamily(); + +/// See also [ItemsNotifier]. +class ItemsNotifierFamily extends Family>> { + /// See also [ItemsNotifier]. + const ItemsNotifierFamily(); + + /// See also [ItemsNotifier]. + ItemsNotifierProvider call( + String list, + ) { + return ItemsNotifierProvider( + list, + ); + } + + @override + ItemsNotifierProvider getProviderOverride( + covariant ItemsNotifierProvider provider, + ) { + return call( + provider.list, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'itemsNotifierProvider'; +} + +/// See also [ItemsNotifier]. +class ItemsNotifierProvider extends AutoDisposeStreamNotifierProviderImpl< + ItemsNotifier, List> { + /// See also [ItemsNotifier]. + ItemsNotifierProvider( + String list, + ) : this._internal( + () => ItemsNotifier()..list = list, + from: itemsNotifierProvider, + name: r'itemsNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$itemsNotifierHash, + dependencies: ItemsNotifierFamily._dependencies, + allTransitiveDependencies: + ItemsNotifierFamily._allTransitiveDependencies, + list: list, + ); + + ItemsNotifierProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.list, + }) : super.internal(); + + final String list; + + @override + Stream> runNotifierBuild( + covariant ItemsNotifier notifier, + ) { + return notifier.build( + list, + ); + } + + @override + Override overrideWith(ItemsNotifier Function() create) { + return ProviderOverride( + origin: this, + override: ItemsNotifierProvider._internal( + () => create()..list = list, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + list: list, + ), + ); + } + + @override + AutoDisposeStreamNotifierProviderElement> + createElement() { + return _ItemsNotifierProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is ItemsNotifierProvider && other.list == list; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, list.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin ItemsNotifierRef on AutoDisposeStreamNotifierProviderRef> { + /// The parameter `list` of this provider. + String get list; +} + +class _ItemsNotifierProviderElement + extends AutoDisposeStreamNotifierProviderElement> with ItemsNotifierRef { + _ItemsNotifierProviderElement(super.provider); + + @override + String get list => (origin as ItemsNotifierProvider).list; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/demos/supabase-todolist-drift/lib/stores/lists.dart b/demos/supabase-todolist-drift/lib/stores/lists.dart new file mode 100644 index 00000000..cf47f0bd --- /dev/null +++ b/demos/supabase-todolist-drift/lib/stores/lists.dart @@ -0,0 +1,33 @@ +import 'package:drift/drift.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../powersync/database.dart'; +import '../supabase.dart'; + +part 'lists.g.dart'; + +@riverpod +final class ListsNotifier extends _$ListsNotifier { + @override + Stream> build() { + final database = ref.watch(driftDatabase); + return database.listsWithStats().watch(); + } + + Future createNewList(String name) async { + final database = ref.read(driftDatabase); + await database.listItems.insertOne(ListItemsCompanion.insert( + name: name, + ownerId: Value(ref.read(userIdProvider)), + )); + } + + Future deleteList(String id) async { + // We only need to delete the list here, the foreign key constraint on the + // server will delete related todos (which will delete them locally after + // the next sync). + final database = ref.read(driftDatabase); + final stmt = database.listItems.delete()..where((row) => row.id.equals(id)); + await stmt.go(); + } +} diff --git a/demos/supabase-todolist-drift/lib/stores/lists.g.dart b/demos/supabase-todolist-drift/lib/stores/lists.g.dart new file mode 100644 index 00000000..ac722cf4 --- /dev/null +++ b/demos/supabase-todolist-drift/lib/stores/lists.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'lists.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$listsNotifierHash() => r'6cefeae1ff39373c3b827895be5b1d6911cfe023'; + +/// See also [ListsNotifier]. +@ProviderFor(ListsNotifier) +final listsNotifierProvider = AutoDisposeStreamNotifierProvider>.internal( + ListsNotifier.new, + name: r'listsNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$listsNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$ListsNotifier = AutoDisposeStreamNotifier>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/demos/supabase-todolist-drift/lib/supabase.dart b/demos/supabase-todolist-drift/lib/supabase.dart index e4e4d05b..db28c6e0 100644 --- a/demos/supabase-todolist-drift/lib/supabase.dart +++ b/demos/supabase-todolist-drift/lib/supabase.dart @@ -1,6 +1,13 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logging/logging.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:stream_transform/stream_transform.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'app_config.dart'; +import 'powersync/powersync.dart'; + +part 'supabase.g.dart'; loadSupabase() async { await Supabase.initialize( @@ -8,3 +15,65 @@ loadSupabase() async { anonKey: AppConfig.supabaseAnonKey, ); } + +@riverpod +Stream session(Ref ref) { + final instance = Supabase.instance.client.auth; + + return instance.onAuthStateChange + .map((_) => instance.currentSession) + .startWith(instance.currentSession); +} + +@riverpod +bool isLoggedIn(Ref ref) { + return ref.watch(sessionProvider.select((session) => session.value != null)); +} + +@riverpod +String? userId(Ref ref) { + return ref.watch(sessionProvider.select((session) => session.value?.user.id)); +} + +typedef AuthState = ({String? error, bool isBusy}); + +@riverpod +final class AuthNotifier extends _$AuthNotifier { + static final _logger = Logger('AuthNotifier'); + + @override + AuthState build() { + return (error: null, isBusy: false); + } + + Future _doWork(Future Function() inner) async { + try { + state = (error: null, isBusy: true); + await inner(); + state = (error: null, isBusy: false); + } catch (e, s) { + _logger.warning('auth error', e, s); + state = (error: e.toString(), isBusy: false); + } + } + + Future login(String username, String password) { + return _doWork(() async { + await Supabase.instance.client.auth + .signInWithPassword(email: username, password: password); + }); + } + + Future signup(String username, String password) async { + return _doWork(() async { + await Supabase.instance.client.auth + .signUp(email: username, password: password); + }); + } + + Future signOut() async { + await Supabase.instance.client.auth.signOut(); + await (await ref.read(powerSyncInstanceProvider.future)) + .disconnectAndClear(); + } +} diff --git a/demos/supabase-todolist-drift/lib/supabase.g.dart b/demos/supabase-todolist-drift/lib/supabase.g.dart new file mode 100644 index 00000000..d72036f6 --- /dev/null +++ b/demos/supabase-todolist-drift/lib/supabase.g.dart @@ -0,0 +1,73 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'supabase.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$sessionHash() => r'1ecba22c88e6f2c7349d9da812430647fe008045'; + +/// See also [session]. +@ProviderFor(session) +final sessionProvider = AutoDisposeStreamProvider.internal( + session, + name: r'sessionProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$sessionHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef SessionRef = AutoDisposeStreamProviderRef; +String _$isLoggedInHash() => r'1d50e28b5449cd3d195c0736f5f9d92b97e69cc8'; + +/// See also [isLoggedIn]. +@ProviderFor(isLoggedIn) +final isLoggedInProvider = AutoDisposeProvider.internal( + isLoggedIn, + name: r'isLoggedInProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$isLoggedInHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef IsLoggedInRef = AutoDisposeProviderRef; +String _$userIdHash() => r'0ca9244c1352c59ea306e9e23278b952eb348681'; + +/// See also [userId]. +@ProviderFor(userId) +final userIdProvider = AutoDisposeProvider.internal( + userId, + name: r'userIdProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$userIdHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef UserIdRef = AutoDisposeProviderRef; +String _$authNotifierHash() => r'ed547c8adf5eb1a61014332a83dcd266e47b25b3'; + +/// See also [AuthNotifier]. +@ProviderFor(AuthNotifier) +final authNotifierProvider = + AutoDisposeNotifierProvider.internal( + AuthNotifier.new, + name: r'authNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$authNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$AuthNotifier = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/demos/supabase-todolist-drift/lib/utils/provider_observer.dart b/demos/supabase-todolist-drift/lib/utils/provider_observer.dart new file mode 100644 index 00000000..c47c70e9 --- /dev/null +++ b/demos/supabase-todolist-drift/lib/utils/provider_observer.dart @@ -0,0 +1,22 @@ +import 'package:logging/logging.dart'; +import 'package:riverpod/riverpod.dart'; + +final class LoggingProviderObserver extends ProviderObserver { + static final _log = Logger('provider'); + + const LoggingProviderObserver(); + + @override + void didUpdateProvider(ProviderBase provider, Object? previousValue, + Object? newValue, ProviderContainer container) { + if (newValue case AsyncError(:final error, :final stackTrace)) { + _log.warning('$provider emitted async error', error, stackTrace); + } + } + + @override + void providerDidFail(ProviderBase provider, Object error, + StackTrace stackTrace, ProviderContainer container) { + _log.warning('$provider threw exception', error, stackTrace); + } +} diff --git a/demos/supabase-todolist-drift/lib/widgets/fts_search_delegate.dart b/demos/supabase-todolist-drift/lib/widgets/fts_search_delegate.dart deleted file mode 100644 index 521ef0e6..00000000 --- a/demos/supabase-todolist-drift/lib/widgets/fts_search_delegate.dart +++ /dev/null @@ -1,112 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:logging/logging.dart'; -import 'package:supabase_todolist_drift/database.dart'; -import 'package:supabase_todolist_drift/fts_helpers.dart' as fts_helpers; -import 'package:supabase_todolist_drift/powersync.dart'; - -import 'todo_list_page.dart'; - -final log = Logger('powersync-supabase'); - -class FtsSearchDelegate extends SearchDelegate { - @override - List? buildActions(BuildContext context) { - return [ - IconButton( - onPressed: () { - query = ''; - }, - icon: const Icon(Icons.clear), - ), - ]; - } - - @override - Widget? buildLeading(BuildContext context) { - return IconButton( - onPressed: () { - close(context, null); - }, - icon: const Icon(Icons.arrow_back), - ); - } - - @override - Widget buildResults(BuildContext context) { - return FutureBuilder( - future: _search(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - return ListView.builder( - itemBuilder: (context, index) { - return ListTile( - title: Text(snapshot.data?[index].name), - onTap: () { - close(context, null); - }, - ); - }, - itemCount: snapshot.data?.length, - ); - } else { - return const Center( - child: CircularProgressIndicator(), - ); - } - }, - ); - } - - @override - Widget buildSuggestions(BuildContext context) { - NavigatorState navigator = Navigator.of(context); - - return FutureBuilder( - future: _search(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - return ListView.builder( - itemBuilder: (context, index) { - return ListTile( - title: Text(snapshot.data?[index]['name'] ?? ''), - onTap: () async { - ListItem list = - await appDb.findList(snapshot.data![index]['id']); - navigator.push(MaterialPageRoute( - builder: (context) => TodoListPage(list: list), - )); - }, - ); - }, - itemCount: snapshot.data?.length, - ); - } else { - return const Center( - child: CircularProgressIndicator(), - ); - } - }, - ); - } - - Future _search() async { - if (query.isEmpty) return []; - List listsSearchResults = await fts_helpers.search(query, 'lists'); - List todoItemsSearchResults = await fts_helpers.search(query, 'todos'); - List formattedListResults = listsSearchResults - .map((result) => {"id": result['id'], "name": result['name']}) - .toList(); - List formattedTodoItemsResults = todoItemsSearchResults - .map((result) => { - // Use list_id so the navigation goes to the list page - "id": result['list_id'], - "name": result['description'], - }) - .toList(); - List formattedResults = [ - ...formattedListResults, - ...formattedTodoItemsResults - ]; - return formattedResults; - } -} diff --git a/demos/supabase-todolist-drift/lib/widgets/lists_page.dart b/demos/supabase-todolist-drift/lib/widgets/lists_page.dart deleted file mode 100644 index 52e3beed..00000000 --- a/demos/supabase-todolist-drift/lib/widgets/lists_page.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:supabase_todolist_drift/database.dart'; -import 'package:supabase_todolist_drift/powersync.dart'; - -import 'list_item.dart'; -import 'list_item_dialog.dart'; -import '../main.dart'; - -void _showAddDialog(BuildContext context) async { - return showDialog( - context: context, - barrierDismissible: false, // user must tap button! - builder: (BuildContext context) { - return const ListItemDialog(); - }, - ); -} - -class ListsPage extends StatelessWidget { - const ListsPage({super.key}); - - @override - Widget build(BuildContext context) { - const content = ListsWidget(); - - final button = FloatingActionButton( - onPressed: () { - _showAddDialog(context); - }, - tooltip: 'Create List', - child: const Icon(Icons.add), - ); - - final page = MyHomePage( - title: 'Todo Lists', - content: content, - floatingActionButton: button, - ); - return page; - } -} - -class ListsWidget extends StatefulWidget { - const ListsWidget({super.key}); - - @override - State createState() { - return _ListsWidgetState(); - } -} - -class _ListsWidgetState extends State { - List _data = []; - bool hasSynced = false; - StreamSubscription? _subscription; - StreamSubscription? _syncStatusSubscription; - - _ListsWidgetState(); - - @override - void initState() { - super.initState(); - final stream = appDb.watchListsWithStats(); - _subscription = stream.listen((data) { - if (!context.mounted) { - return; - } - setState(() { - _data = data; - }); - }); - _syncStatusSubscription = db.statusStream.listen((status) { - if (!context.mounted) { - return; - } - setState(() { - hasSynced = status.hasSynced ?? false; - }); - }); - } - - @override - void dispose() { - super.dispose(); - _subscription?.cancel(); - _syncStatusSubscription?.cancel(); - } - - @override - Widget build(BuildContext context) { - return !hasSynced - ? const Text("Busy with sync...") - : ListView( - padding: const EdgeInsets.symmetric(vertical: 8.0), - children: _data.map((list) { - return ListItemWidget(list: list); - }).toList(), - ); - } -} diff --git a/demos/supabase-todolist-drift/lib/widgets/todo_list_page.dart b/demos/supabase-todolist-drift/lib/widgets/todo_list_page.dart deleted file mode 100644 index f9687bd7..00000000 --- a/demos/supabase-todolist-drift/lib/widgets/todo_list_page.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:supabase_todolist_drift/database.dart'; -import 'package:supabase_todolist_drift/powersync.dart'; - -import 'status_app_bar.dart'; -import 'todo_item_dialog.dart'; -import 'todo_item_widget.dart'; - -void _showAddDialog(BuildContext context, ListItem list) async { - return showDialog( - context: context, - barrierDismissible: false, // user must tap button! - builder: (BuildContext context) { - return TodoItemDialog(list: list); - }, - ); -} - -class TodoListPage extends StatelessWidget { - final ListItem list; - - const TodoListPage({super.key, required this.list}); - - @override - Widget build(BuildContext context) { - final button = FloatingActionButton( - onPressed: () { - _showAddDialog(context, list); - }, - tooltip: 'Add Item', - child: const Icon(Icons.add), - ); - - return Scaffold( - appBar: StatusAppBar(title: list.name), - floatingActionButton: button, - body: TodoListWidget(list: list)); - } -} - -class TodoListWidget extends StatefulWidget { - final ListItem list; - - const TodoListWidget({super.key, required this.list}); - - @override - State createState() { - return TodoListWidgetState(); - } -} - -class TodoListWidgetState extends State { - List _data = []; - StreamSubscription? _subscription; - - TodoListWidgetState(); - - @override - void initState() { - super.initState(); - final stream = appDb.watchTodoItems(widget.list); - _subscription = stream.listen((data) { - if (!context.mounted) { - return; - } - setState(() { - _data = data; - }); - }); - } - - @override - void dispose() { - super.dispose(); - _subscription?.cancel(); - } - - @override - Widget build(BuildContext context) { - return ListView( - padding: const EdgeInsets.symmetric(vertical: 8.0), - children: _data.map((todo) { - return TodoItemWidget(todo: todo); - }).toList(), - ); - } -} diff --git a/demos/supabase-todolist-drift/macos/Runner.xcodeproj/project.pbxproj b/demos/supabase-todolist-drift/macos/Runner.xcodeproj/project.pbxproj index c2cf4ce2..5c87fe22 100644 --- a/demos/supabase-todolist-drift/macos/Runner.xcodeproj/project.pbxproj +++ b/demos/supabase-todolist-drift/macos/Runner.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -64,7 +65,7 @@ 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* supabase_todolist_drift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "supabase_todolist_drift.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* supabase_todolist_drift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = supabase_todolist_drift.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -76,6 +77,7 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ @@ -92,6 +94,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -124,7 +127,6 @@ 33CEB47122A05771004F2AC0 /* Flutter */, 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, ); sourceTree = ""; }; @@ -151,6 +153,7 @@ 33CEB47122A05771004F2AC0 /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, @@ -172,13 +175,6 @@ path = Runner; sourceTree = ""; }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -216,6 +212,9 @@ 33CC11202044C79F0003C045 /* PBXTargetDependency */, ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 33CC10ED2044A3C60003C045 /* supabase_todolist_drift.app */; productType = "com.apple.product-type.application"; @@ -260,6 +259,9 @@ Base, ); mainGroup = 33CC10E42044A3C60003C045; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -461,7 +463,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -543,7 +545,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -593,7 +595,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -700,6 +702,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } diff --git a/demos/supabase-todolist-drift/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/demos/supabase-todolist-drift/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..759d5a05 --- /dev/null +++ b/demos/supabase-todolist-drift/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,22 @@ +{ + "pins" : [ + { + "identity" : "csqlite", + "kind" : "remoteSourceControl", + "location" : "https://github.com/simolus3/CSQLite.git", + "state" : { + "revision" : "a8d28afef08ad8faa4ee9ef7845f61c2e8ac5810" + } + }, + { + "identity" : "powersync-sqlite-core-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/powersync-ja/powersync-sqlite-core-swift.git", + "state" : { + "revision" : "b2a81af14e9ad83393eb187bb02e62e6db8b5ad6", + "version" : "0.4.6" + } + } + ], + "version" : 2 +} diff --git a/demos/supabase-todolist-drift/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demos/supabase-todolist-drift/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 906d1c2b..6b3005f0 100644 --- a/demos/supabase-todolist-drift/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/demos/supabase-todolist-drift/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + diff --git a/demos/supabase-todolist-drift/macos/Runner.xcworkspace/contents.xcworkspacedata b/demos/supabase-todolist-drift/macos/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16..21a3cc14 100644 --- a/demos/supabase-todolist-drift/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/demos/supabase-todolist-drift/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/demos/supabase-todolist-drift/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/demos/supabase-todolist-drift/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..759d5a05 --- /dev/null +++ b/demos/supabase-todolist-drift/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,22 @@ +{ + "pins" : [ + { + "identity" : "csqlite", + "kind" : "remoteSourceControl", + "location" : "https://github.com/simolus3/CSQLite.git", + "state" : { + "revision" : "a8d28afef08ad8faa4ee9ef7845f61c2e8ac5810" + } + }, + { + "identity" : "powersync-sqlite-core-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/powersync-ja/powersync-sqlite-core-swift.git", + "state" : { + "revision" : "b2a81af14e9ad83393eb187bb02e62e6db8b5ad6", + "version" : "0.4.6" + } + } + ], + "version" : 2 +} diff --git a/demos/supabase-todolist-drift/macos/Runner/AppDelegate.swift b/demos/supabase-todolist-drift/macos/Runner/AppDelegate.swift index d53ef643..b3c17614 100644 --- a/demos/supabase-todolist-drift/macos/Runner/AppDelegate.swift +++ b/demos/supabase-todolist-drift/macos/Runner/AppDelegate.swift @@ -1,9 +1,13 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } } diff --git a/demos/supabase-todolist-drift/macos/Runner/DebugProfile.entitlements b/demos/supabase-todolist-drift/macos/Runner/DebugProfile.entitlements index dddb8a30..3ba6c126 100644 --- a/demos/supabase-todolist-drift/macos/Runner/DebugProfile.entitlements +++ b/demos/supabase-todolist-drift/macos/Runner/DebugProfile.entitlements @@ -6,6 +6,8 @@ com.apple.security.cs.allow-jit + com.apple.security.network.client + com.apple.security.network.server diff --git a/demos/supabase-todolist-drift/macos/Runner/Release.entitlements b/demos/supabase-todolist-drift/macos/Runner/Release.entitlements index 852fa1a4..ee95ab7e 100644 --- a/demos/supabase-todolist-drift/macos/Runner/Release.entitlements +++ b/demos/supabase-todolist-drift/macos/Runner/Release.entitlements @@ -4,5 +4,7 @@ com.apple.security.app-sandbox + com.apple.security.network.client + diff --git a/demos/supabase-todolist-drift/pubspec.lock b/demos/supabase-todolist-drift/pubspec.lock index 5a70fd5f..ba153676 100644 --- a/demos/supabase-todolist-drift/pubspec.lock +++ b/demos/supabase-todolist-drift/pubspec.lock @@ -5,34 +5,34 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 url: "https://pub.dev" source: hosted - version: "67.0.0" + version: "80.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "7.3.0" analyzer_plugin: dependency: transitive description: name: analyzer_plugin - sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" + sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4 url: "https://pub.dev" source: hosted - version: "0.11.3" + version: "0.13.0" app_links: dependency: transitive description: name: app_links - sha256: ae5f9a1b7d40d26178f605414be81ed4260350b4fae8259fe5ca4f89fe70c4af + sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "6.4.0" app_links_linux: dependency: transitive description: @@ -61,82 +61,98 @@ packages: dependency: transitive description: name: archive - sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" url: "https://pub.dev" source: hosted - version: "3.6.1" + version: "4.0.7" args: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.7.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + auto_route: + dependency: "direct main" + description: + name: auto_route + sha256: "89bc5d17d8c575399891194b8cd02b39f52a8512c730052f17ebe443cdcb9109" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "10.0.1" + auto_route_generator: + dependency: "direct dev" + description: + name: auto_route_generator + sha256: "8e622d26dc6be4bf496d47969e3e9ba555c3abcf2290da6abfa43cbd4f57fa52" + url: "https://pub.dev" + source: hosted + version: "10.0.1" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" build: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" build_config: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.4" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" + sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" url: "https://pub.dev" source: hosted - version: "2.4.11" + version: "2.4.15" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.3.1" + version: "8.0.0" built_collection: dependency: transitive description: @@ -149,10 +165,10 @@ packages: dependency: transitive description: name: built_value - sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 url: "https://pub.dev" source: hosted - version: "8.9.2" + version: "8.9.5" camera: dependency: "direct main" description: @@ -165,50 +181,50 @@ packages: dependency: transitive description: name: camera_android - sha256: "134b83167cc3c83199e8d75e5bcfde677fec843e7b2ca6b754a5b0b96d00d921" + sha256: "08808be7e26fc3c7426c81b3fa387564b8e9c22e6fe9cb5675ce3ab7017d8203" url: "https://pub.dev" source: hosted - version: "0.10.9+10" + version: "0.10.10+3" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: b5093a82537b64bb88d4244f8e00b5ba69e822a5994f47b31d11400e1db975e5 + sha256: ca36181194f429eef3b09de3c96280f2400693f9735025f90d1f4a27465fdd72 url: "https://pub.dev" source: hosted - version: "0.9.17+1" + version: "0.9.19" camera_platform_interface: dependency: transitive description: name: camera_platform_interface - sha256: b3ede1f171532e0d83111fe0980b46d17f1aa9788a07a2fbed07366bbdbb9061 + sha256: "2f757024a48696ff4814a789b0bd90f5660c0fb25f393ab4564fb483327930e2" url: "https://pub.dev" source: hosted - version: "2.8.0" + version: "2.10.0" camera_web: dependency: transitive description: name: camera_web - sha256: b9235ec0a2ce949daec546f1f3d86f05c3921ed31c7d9ab6b7c03214d152fc2d + sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f" url: "https://pub.dev" source: hosted - version: "0.3.4" + version: "0.3.5" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" charcode: dependency: transitive description: name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -221,42 +237,42 @@ packages: dependency: transitive description: name: cli_util - sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c url: "https://pub.dev" source: hosted - version: "0.4.1" + version: "0.4.2" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" code_builder: dependency: transitive description: name: code_builder - sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.10.1" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" convert: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" cross_file: dependency: transitive description: @@ -269,95 +285,103 @@ packages: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" + custom_lint_core: + dependency: transitive + description: + name: custom_lint_core + sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be" + url: "https://pub.dev" + source: hosted + version: "0.7.5" + custom_lint_visitor: + dependency: transitive + description: + name: custom_lint_visitor + sha256: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8" + url: "https://pub.dev" + source: hosted + version: "1.0.0+7.3.0" dart_style: dependency: transitive description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "3.0.1" drift: dependency: "direct main" description: name: drift - sha256: d6ff1ec6a0f3fa097dda6b776cf601f1f3d88b53b287288e09c1306f394fb1b3 + sha256: "6aaea757f53bb035e8a3baedf3d1d53a79d6549a6c13d84f7546509da9372c7c" url: "https://pub.dev" source: hosted - version: "2.20.3" + version: "2.28.1" drift_dev: dependency: "direct dev" description: name: drift_dev - sha256: "3ee987578ca2281b5ff91eadd757cd6dd36001458d6e33784f990d67ff38f756" + sha256: "68c138e884527d2bd61df2ade276c3a144df84d1adeb0ab8f3196b5afe021bd4" url: "https://pub.dev" source: hosted - version: "2.20.3" + version: "2.28.0" drift_sqlite_async: dependency: "direct main" description: name: drift_sqlite_async - sha256: b29c9a838d2ed3f285c5964bc57c157af2a826cfa6dd3bf1b46121b15bb1212d + sha256: "7080bb93f042fe71b5870db999b1ebf8ae61fc2faa3d8617d770055f75a85b17" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.2.3+1" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" - fetch_api: - dependency: transitive - description: - name: fetch_api - sha256: "97f46c25b480aad74f7cc2ad7ccba2c5c6f08d008e68f95c1077286ce243d0e6" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - fetch_client: - dependency: transitive - description: - name: fetch_client - sha256: "9666ee14536778474072245ed5cba07db81ae8eb5de3b7bf4a2d1e2c49696092" - url: "https://pub.dev" - source: hosted - version: "1.1.2" + version: "1.3.3" ffi: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_hooks: + dependency: "direct main" + description: + name: flutter_hooks + sha256: b772e710d16d7a20c0740c4f855095026b31c7eb5ba3ab67d2bd52021cd9461d + url: "https://pub.dev" + source: hosted + version: "0.21.2" flutter_lints: dependency: "direct dev" description: @@ -370,10 +394,18 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "9d98bd47ef9d34e803d438f17fd32b116d31009f534a6fa5ce3a1167f189a6de" + sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e + url: "https://pub.dev" + source: hosted + version: "2.0.28" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" url: "https://pub.dev" source: hosted - version: "2.0.21" + version: "2.6.1" flutter_test: dependency: "direct dev" description: flutter @@ -384,6 +416,14 @@ packages: description: flutter source: sdk version: "0.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b + url: "https://pub.dev" + source: hosted + version: "3.0.0" frontend_server_client: dependency: transitive description: @@ -396,26 +436,26 @@ packages: dependency: transitive description: name: functions_client - sha256: "229648c4c78e0cb13dfb5e7508a3d2e7058f126ba364b94f8199ba4cead96d4e" + sha256: b410e4d609522357396cd84bb9a8f6e3a4561b5f7d3ce82267f6f1c2af42f16b url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.2" glob: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" gotrue: dependency: transitive description: name: gotrue - sha256: a8784341bcc08f88ba7a4b04a40a37059c7e71c315f058d45c31d09e8a951194 + sha256: "04a6efacffd42773ed96dc752f19bb20a1fbc383e81ba82659072b775cf62912" url: "https://pub.dev" source: hosted - version: "2.8.3" + version: "2.12.0" graphs: dependency: transitive description: @@ -432,54 +472,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + hooks_riverpod: + dependency: "direct main" + description: + name: hooks_riverpod + sha256: "70bba33cfc5670c84b796e6929c54b8bc5be7d0fe15bb28c2560500b9ad06966" + url: "https://pub.dev" + source: hosted + version: "2.6.1" http: dependency: transitive description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.5.0" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" image: dependency: "direct main" description: name: image - sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.5.4" io: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" js: dependency: transitive description: name: js - sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "0.7.2" json_annotation: dependency: transitive description: @@ -500,26 +548,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "11.0.1" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -532,18 +580,18 @@ packages: dependency: "direct main" description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -556,18 +604,18 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mime: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "2.0.0" mutex: dependency: transitive description: @@ -580,42 +628,42 @@ packages: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" path: dependency: "direct main" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_provider: dependency: "direct main" description: name: path_provider - sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "490539678396d4c3c0b06efdaab75ae60675c3e0c66f72bc04c2e2c1e0e2abeb" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.9" + version: "2.2.17" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -644,18 +692,18 @@ packages: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.1.0" platform: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -672,59 +720,74 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" + posix: + dependency: transitive + description: + name: posix + sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62 + url: "https://pub.dev" + source: hosted + version: "6.0.2" postgrest: dependency: transitive description: name: postgrest - sha256: "675540a5e93e89fc28cc4ef0999ed2916c00a1c73de73f3a0d5067ce36dd41a2" + sha256: "10b81a23b1c829ccadf68c626b4d66666453a1474d24c563f313f5ca7851d575" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.4.2" powersync: dependency: "direct main" description: path: "../../packages/powersync" relative: true source: path - version: "1.9.2" + version: "1.15.2" powersync_attachments_helper: dependency: "direct main" description: path: "../../packages/powersync_attachments_helper" relative: true source: path - version: "0.6.15+1" + version: "0.6.19" + powersync_core: + dependency: "direct overridden" + description: + path: "../../packages/powersync_core" + relative: true + source: path + version: "1.5.2" powersync_flutter_libs: dependency: "direct overridden" description: path: "../../packages/powersync_flutter_libs" relative: true source: path - version: "0.4.2" + version: "0.4.11" pub_semver: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" realtime_client: dependency: transitive description: name: realtime_client - sha256: a99b7817e203c57ada746e9fe113820410cf84d9029f4310c57737aae890b0f7 + sha256: "3a0a99b5bd0fc3b35e8ee846d9a22fa2c2117f7ef1cb73d1e5f08f6c3d09c4e9" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.5.0" recase: dependency: transitive description: @@ -741,107 +804,139 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + riverpod: + dependency: "direct main" + description: + name: riverpod + sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + riverpod_analyzer_utils: + dependency: transitive + description: + name: riverpod_analyzer_utils + sha256: "03a17170088c63aab6c54c44456f5ab78876a1ddb6032ffde1662ddab4959611" + url: "https://pub.dev" + source: hosted + version: "0.5.10" + riverpod_annotation: + dependency: "direct main" + description: + name: riverpod_annotation + sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8 + url: "https://pub.dev" + source: hosted + version: "2.6.1" + riverpod_generator: + dependency: "direct dev" + description: + name: riverpod_generator + sha256: "44a0992d54473eb199ede00e2260bd3c262a86560e3c6f6374503d86d0580e36" + url: "https://pub.dev" + source: hosted + version: "2.6.5" rxdart: dependency: transitive description: name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" url: "https://pub.dev" source: hosted - version: "0.27.7" + version: "0.28.0" shared_preferences: dependency: transitive description: name: shared_preferences - sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "3d4571b3c5eb58ce52a419d86e655493d0bc3020672da79f72fa0c16ca3a8ec1" + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.4.10" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.5.4" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "034650b71e73629ca08a0bd789fd1d83cc63c2d1e405946f7cef7bc37432f93a" + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shelf: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "3.0.0" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_gen: dependency: transitive description: name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "2.0.0" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" sprintf: dependency: transitive description: @@ -854,130 +949,138 @@ packages: dependency: transitive description: name: sqlite3 - sha256: bb174b3ec2527f9c5f680f73a89af8149dd99782fbb56ea88ad0807c5638f2ed + sha256: f393d92c71bdcc118d6203d07c991b9be0f84b1a6f89dd4f7eed348131329924 url: "https://pub.dev" source: hosted - version: "2.4.7" + version: "2.9.0" sqlite3_flutter_libs: dependency: transitive description: name: sqlite3_flutter_libs - sha256: "62bbb4073edbcdf53f40c80775f33eea01d301b7b81417e5b3fb7395416258c1" + sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14" url: "https://pub.dev" source: hosted - version: "0.5.24" + version: "0.5.32" sqlite3_web: dependency: transitive description: name: sqlite3_web - sha256: f22d1dda7a40be0867984f55cdf5c2d599e5f05d3be4a642d78f38b38983f554 + sha256: "0f6ebcb4992d1892ac5c8b5ecd22a458ab9c5eb6428b11ae5ecb5d63545844da" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.3.2" sqlite_async: dependency: "direct main" description: name: sqlite_async - sha256: d66fb6e6d07c1a834743326c033029f75becbb1fad6823d709f921872abc3d5b + sha256: "6116bfc6aef6ce77730b478385ba4a58873df45721f6a9bc6ffabf39b6576e36" url: "https://pub.dev" source: hosted - version: "0.11.0" + version: "0.12.1" sqlparser: dependency: transitive description: name: sqlparser - sha256: "852cf80f9e974ac8e1b613758a8aa640215f7701352b66a7f468e95711eb570b" + sha256: "7c859c803cf7e9a84d6db918bac824545045692bbe94a6386bd3a45132235d09" url: "https://pub.dev" source: hosted - version: "0.38.1" + version: "0.41.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" storage_client: dependency: transitive description: name: storage_client - sha256: e37f1b9d40f43078d12bd2d1b6b08c2c16fbdbafc58b57bc44922da6ea3f5625 + sha256: "09bac4d75eea58e8113ca928e6655a09cc8059e6d1b472ee801f01fde815bcfc" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.4.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: - dependency: transitive + dependency: "direct main" description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" supabase: dependency: transitive description: name: supabase - sha256: f6d21d0b555aa997d8170afd112407df0aaf44820cbe93b3520e2f7ea3ec0904 + sha256: f00172f5f0b2148ea1c573f52862d50cacb6f353f579f741fa35e51704845958 url: "https://pub.dev" source: hosted - version: "2.2.7" + version: "2.7.0" supabase_flutter: dependency: "direct main" description: name: supabase_flutter - sha256: bcac7745e423ffe4aea86fefc3ccef259f979005c7a40558089c11f8606fd085 + sha256: d88eccf9e46e57129725a08e72a3109b6f780921fdc27fe3d7669a11ae80906b url: "https://pub.dev" source: hosted - version: "2.5.11" + version: "2.9.0" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.6" timing: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" typed_data: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" universal_io: dependency: "direct main" description: @@ -990,42 +1093,42 @@ packages: dependency: transitive description: name: url_launcher - sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "678979703e10d7862c551c736fe6b9f185261bddf141b46672063b99790bc700" + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" url: "https://pub.dev" source: hosted - version: "6.3.7" + version: "6.3.16" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.3" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: @@ -1038,74 +1141,82 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.1" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.4" uuid: dependency: transitive description: name: uuid - sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "4.4.2" + version: "4.5.1" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "15.0.0" watcher: dependency: transitive description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "3.0.3" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: @@ -1118,18 +1229,18 @@ packages: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" yet_another_json_isolate: dependency: transitive description: name: yet_another_json_isolate - sha256: "381dd593e4199fe41a7b6467cef77a41d91fa8ef41465e135f12c99a76be463b" + sha256: fe45897501fa156ccefbfb9359c9462ce5dec092f05e8a56109db30be864f01e url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + dart: ">=3.8.0-0 <4.0.0" + flutter: ">=3.27.0" diff --git a/demos/supabase-todolist-drift/pubspec.yaml b/demos/supabase-todolist-drift/pubspec.yaml index 22418c30..051931b2 100644 --- a/demos/supabase-todolist-drift/pubspec.yaml +++ b/demos/supabase-todolist-drift/pubspec.yaml @@ -9,8 +9,8 @@ environment: dependencies: flutter: sdk: flutter - powersync_attachments_helper: ^0.6.15+2 - powersync: ^1.9.3 + powersync_attachments_helper: ^0.6.20 + powersync: ^1.16.1 path_provider: ^2.1.1 supabase_flutter: ^2.0.1 path: ^1.8.3 @@ -18,9 +18,16 @@ dependencies: camera: ^0.10.5+7 image: ^4.1.3 universal_io: ^2.2.2 - sqlite_async: ^0.11.0 + sqlite_async: ^0.12.0 drift: ^2.20.2 drift_sqlite_async: ^0.2.0 + riverpod_annotation: ^2.6.1 + riverpod: ^2.6.1 + flutter_hooks: ^0.21.2 + hooks_riverpod: ^2.6.1 + flutter_riverpod: ^2.6.1 + auto_route: ^10.0.1 + stream_transform: ^2.1.1 dev_dependencies: flutter_test: @@ -29,6 +36,10 @@ dev_dependencies: flutter_lints: ^3.0.1 drift_dev: ^2.20.3 build_runner: ^2.4.8 + riverpod_generator: ^2.6.5 + auto_route_generator: ^10.0.1 flutter: uses-material-design: true + config: + enable-swift-package-manager: true diff --git a/demos/supabase-todolist-optional-sync/.gitignore b/demos/supabase-todolist-optional-sync/.gitignore index 823033ae..e880a370 100644 --- a/demos/supabase-todolist-optional-sync/.gitignore +++ b/demos/supabase-todolist-optional-sync/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related diff --git a/demos/supabase-todolist-optional-sync/ios/Podfile b/demos/supabase-todolist-optional-sync/ios/Podfile index e9f73048..2c1e086a 100644 --- a/demos/supabase-todolist-optional-sync/ios/Podfile +++ b/demos/supabase-todolist-optional-sync/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '12.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/demos/supabase-todolist-optional-sync/ios/Podfile.lock b/demos/supabase-todolist-optional-sync/ios/Podfile.lock index 042d832d..146ca38d 100644 --- a/demos/supabase-todolist-optional-sync/ios/Podfile.lock +++ b/demos/supabase-todolist-optional-sync/ios/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - app_links (0.0.1): + - app_links (0.0.2): - Flutter - camera_avfoundation (0.0.1): - Flutter @@ -7,29 +7,33 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - powersync-sqlite-core (0.3.0) + - powersync-sqlite-core (0.4.5) - powersync_flutter_libs (0.0.1): - Flutter - - powersync-sqlite-core (~> 0.3.0) + - powersync-sqlite-core (~> 0.4.5) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - "sqlite3 (3.46.1+1)": - - "sqlite3/common (= 3.46.1+1)" - - "sqlite3/common (3.46.1+1)" - - "sqlite3/dbstatvtab (3.46.1+1)": + - sqlite3 (3.49.2): + - sqlite3/common (= 3.49.2) + - sqlite3/common (3.49.2) + - sqlite3/dbstatvtab (3.49.2): - sqlite3/common - - "sqlite3/fts5 (3.46.1+1)": + - sqlite3/fts5 (3.49.2): - sqlite3/common - - "sqlite3/perf-threadsafe (3.46.1+1)": + - sqlite3/math (3.49.2): - sqlite3/common - - "sqlite3/rtree (3.46.1+1)": + - sqlite3/perf-threadsafe (3.49.2): + - sqlite3/common + - sqlite3/rtree (3.49.2): - sqlite3/common - sqlite3_flutter_libs (0.0.1): - Flutter - - "sqlite3 (~> 3.46.0+1)" + - FlutterMacOS + - sqlite3 (~> 3.49.1) - sqlite3/dbstatvtab - sqlite3/fts5 + - sqlite3/math - sqlite3/perf-threadsafe - sqlite3/rtree - url_launcher_ios (0.0.1): @@ -42,7 +46,7 @@ DEPENDENCIES: - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - powersync_flutter_libs (from `.symlinks/plugins/powersync_flutter_libs/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: @@ -64,22 +68,22 @@ EXTERNAL SOURCES: shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqlite3_flutter_libs: - :path: ".symlinks/plugins/sqlite3_flutter_libs/ios" + :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: - app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795 - camera_avfoundation: 759172d1a77ae7be0de08fc104cfb79738b8a59e - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - powersync-sqlite-core: ad0e70e23bacd858fe2e79032dc4aabdf972d1bd - powersync_flutter_libs: 064c44b51fb07df9486b735fb96ab7608a89e18b - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb - sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b - url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7 + camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + powersync-sqlite-core: 6f32860379009d2a37cadc9e9427a431bdbd83c8 + powersync_flutter_libs: 7684a62208907328906eb932f1fc8b3d8879974e + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 + sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d -PODFILE CHECKSUM: f7b3cb7384a2d5da4b22b090e1f632de7f377987 +PODFILE CHECKSUM: 2c1730c97ea13f1ea48b32e9c79de785b4f2f02f -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/demos/supabase-todolist-optional-sync/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demos/supabase-todolist-optional-sync/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5e31d3d3..c53e2b31 100644 --- a/demos/supabase-todolist-optional-sync/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/demos/supabase-todolist-optional-sync/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -48,6 +48,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/demos/supabase-todolist-optional-sync/ios/Runner/AppDelegate.swift b/demos/supabase-todolist-optional-sync/ios/Runner/AppDelegate.swift index 70693e4a..b6363034 100644 --- a/demos/supabase-todolist-optional-sync/ios/Runner/AppDelegate.swift +++ b/demos/supabase-todolist-optional-sync/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/demos/supabase-todolist-optional-sync/lib/models/schema.dart b/demos/supabase-todolist-optional-sync/lib/models/schema.dart index de0887b3..7ce1cfd6 100644 --- a/demos/supabase-todolist-optional-sync/lib/models/schema.dart +++ b/demos/supabase-todolist-optional-sync/lib/models/schema.dart @@ -16,7 +16,7 @@ import 'package:powersync_flutter_supabase_todolist_optional_sync_demo/models/sy const todosTable = 'todos'; const listsTable = 'lists'; -Schema makeSchema({synced = bool}) { +Schema makeSchema({required bool synced}) { String syncedName(String table) { if (synced) { // results in lists, todos diff --git a/demos/supabase-todolist-optional-sync/macos/Podfile b/demos/supabase-todolist-optional-sync/macos/Podfile index c795730d..b52666a1 100644 --- a/demos/supabase-todolist-optional-sync/macos/Podfile +++ b/demos/supabase-todolist-optional-sync/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.14' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/demos/supabase-todolist-optional-sync/macos/Podfile.lock b/demos/supabase-todolist-optional-sync/macos/Podfile.lock index 8b4f1b55..6983b2da 100644 --- a/demos/supabase-todolist-optional-sync/macos/Podfile.lock +++ b/demos/supabase-todolist-optional-sync/macos/Podfile.lock @@ -5,29 +5,33 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - powersync-sqlite-core (0.2.1) + - powersync-sqlite-core (0.4.5) - powersync_flutter_libs (0.0.1): - FlutterMacOS - - powersync-sqlite-core (~> 0.2.1) + - powersync-sqlite-core (~> 0.4.5) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - "sqlite3 (3.46.1+1)": - - "sqlite3/common (= 3.46.1+1)" - - "sqlite3/common (3.46.1+1)" - - "sqlite3/dbstatvtab (3.46.1+1)": + - sqlite3 (3.49.2): + - sqlite3/common (= 3.49.2) + - sqlite3/common (3.49.2) + - sqlite3/dbstatvtab (3.49.2): - sqlite3/common - - "sqlite3/fts5 (3.46.1+1)": + - sqlite3/fts5 (3.49.2): - sqlite3/common - - "sqlite3/perf-threadsafe (3.46.1+1)": + - sqlite3/math (3.49.2): - sqlite3/common - - "sqlite3/rtree (3.46.1+1)": + - sqlite3/perf-threadsafe (3.49.2): + - sqlite3/common + - sqlite3/rtree (3.49.2): - sqlite3/common - sqlite3_flutter_libs (0.0.1): + - Flutter - FlutterMacOS - - "sqlite3 (~> 3.46.0+1)" + - sqlite3 (~> 3.49.1) - sqlite3/dbstatvtab - sqlite3/fts5 + - sqlite3/math - sqlite3/perf-threadsafe - sqlite3/rtree - url_launcher_macos (0.0.1): @@ -39,7 +43,7 @@ DEPENDENCIES: - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - powersync_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/powersync_flutter_libs/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`) + - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) SPEC REPOS: @@ -59,21 +63,21 @@ EXTERNAL SOURCES: shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin sqlite3_flutter_libs: - :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos + :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: - app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a - FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - powersync-sqlite-core: 38ead13d8b21920cfbc79e9b3415b833574a506d - powersync_flutter_libs: 3f05f43c382c77cb7bec64785c2b6b1e9bd33c22 - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb - sqlite3_flutter_libs: 5ca46c1a04eddfbeeb5b16566164aa7ad1616e7b - url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 + app_links: afe860c55c7ef176cea7fb630a2b7d7736de591d + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + powersync-sqlite-core: 6f32860379009d2a37cadc9e9427a431bdbd83c8 + powersync_flutter_libs: 41d8a7b193abf15e46f95f0ec1229d86b6893171 + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 + sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 -PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 +PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/demos/supabase-todolist-optional-sync/pubspec.lock b/demos/supabase-todolist-optional-sync/pubspec.lock index 3264656a..26909e2d 100644 --- a/demos/supabase-todolist-optional-sync/pubspec.lock +++ b/demos/supabase-todolist-optional-sync/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: app_links - sha256: a9905d6a60e814503fabc7523a9ed161b812d7ca69c99ad8ceea14279dc4f06b + sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba" url: "https://pub.dev" source: hosted - version: "6.1.3" + version: "6.4.0" app_links_linux: dependency: transitive description: name: app_links_linux - sha256: "567139eca3ca9fb113f2082f3aaa75a26f30f0ebdbe5fa7f09a3913c5bebd630" + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.3" app_links_platform_interface: dependency: transitive description: name: app_links_platform_interface - sha256: "58cff6f11df59b0e514dd5e4a61e988348ad5662f0e75d45d4e214ebea55c94c" + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.2" app_links_web: dependency: transitive description: @@ -37,34 +37,34 @@ packages: dependency: transitive description: name: archive - sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" url: "https://pub.dev" source: hosted - version: "3.6.1" + version: "4.0.7" args: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.7.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" camera: dependency: "direct main" description: @@ -77,42 +77,42 @@ packages: dependency: transitive description: name: camera_android - sha256: "4ef97ae90dab306a4ed8d5eee14c85fd8daf403ae22488b5617c848774396d72" + sha256: "08808be7e26fc3c7426c81b3fa387564b8e9c22e6fe9cb5675ce3ab7017d8203" url: "https://pub.dev" source: hosted - version: "0.10.9+6" + version: "0.10.10+3" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: "7d021e8cd30d9b71b8b92b4ad669e80af432d722d18d6aac338572754a786c15" + sha256: ca36181194f429eef3b09de3c96280f2400693f9735025f90d1f4a27465fdd72 url: "https://pub.dev" source: hosted - version: "0.9.16" + version: "0.9.19" camera_platform_interface: dependency: transitive description: name: camera_platform_interface - sha256: a250314a48ea337b35909a4c9d5416a208d736dcb01d0b02c6af122be66660b0 + sha256: "2f757024a48696ff4814a789b0bd90f5660c0fb25f393ab4564fb483327930e2" url: "https://pub.dev" source: hosted - version: "2.7.4" + version: "2.10.0" camera_web: dependency: transitive description: name: camera_web - sha256: "9e9aba2fbab77ce2472924196ff8ac4dd8f9126c4f9a3096171cd1d870d6b26c" + sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f" url: "https://pub.dev" source: hosted - version: "0.3.3" + version: "0.3.5" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -125,18 +125,18 @@ packages: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" cross_file: dependency: transitive description: @@ -149,58 +149,42 @@ packages: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" - fetch_api: - dependency: transitive - description: - name: fetch_api - sha256: "97f46c25b480aad74f7cc2ad7ccba2c5c6f08d008e68f95c1077286ce243d0e6" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - fetch_client: - dependency: transitive - description: - name: fetch_client - sha256: "9666ee14536778474072245ed5cba07db81ae8eb5de3b7bf4a2d1e2c49696092" - url: "https://pub.dev" - source: hosted - version: "1.1.2" + version: "1.3.3" ffi: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -218,10 +202,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e + sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e url: "https://pub.dev" source: hosted - version: "2.0.20" + version: "2.0.28" flutter_test: dependency: "direct dev" description: flutter @@ -236,18 +220,18 @@ packages: dependency: transitive description: name: functions_client - sha256: "48659e5c6a4bbe02659102bf6406a0cf39142202deae65aacfa78688f2e68946" + sha256: b410e4d609522357396cd84bb9a8f6e3a4561b5f7d3ce82267f6f1c2af42f16b url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.4.2" gotrue: dependency: transitive description: name: gotrue - sha256: "65c8c47afb8230218bc295e6edcb948b117e39801f91c4a4bcb94dfd26b57134" + sha256: "04a6efacffd42773ed96dc752f19bb20a1fbc383e81ba82659072b775cf62912" url: "https://pub.dev" source: hosted - version: "2.8.1" + version: "2.12.0" gtk: dependency: transitive description: @@ -260,34 +244,26 @@ packages: dependency: transitive description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.4.0" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" image: dependency: "direct main" description: name: image - sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" url: "https://pub.dev" source: hosted - version: "4.2.0" - js: - dependency: transitive - description: - name: js - sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf - url: "https://pub.dev" - source: hosted - version: "0.7.1" + version: "4.5.4" json_annotation: dependency: transitive description: @@ -308,18 +284,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -340,18 +316,18 @@ packages: dependency: "direct main" description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -364,18 +340,18 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mime: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "2.0.0" mutex: dependency: transitive description: @@ -388,34 +364,34 @@ packages: dependency: "direct main" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_provider: dependency: "direct main" description: name: path_provider - sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: bca87b0165ffd7cdb9cad8edd22d18d2201e886d9a9f19b4fb3452ea7df3a72a + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.6" + version: "2.2.17" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -436,26 +412,26 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" petitparser: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.1.0" platform: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -464,52 +440,67 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + posix: + dependency: transitive + description: + name: posix + sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62 + url: "https://pub.dev" + source: hosted + version: "6.0.2" postgrest: dependency: transitive description: name: postgrest - sha256: f1f78470a74c611811132ff12acdef9c08b3ec65b61e88161a057d6cc5fbbd83 + sha256: "10b81a23b1c829ccadf68c626b4d66666453a1474d24c563f313f5ca7851d575" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.4.2" powersync: dependency: "direct main" description: path: "../../packages/powersync" relative: true source: path - version: "1.9.2" + version: "1.15.0" + powersync_core: + dependency: "direct overridden" + description: + path: "../../packages/powersync_core" + relative: true + source: path + version: "1.5.0" powersync_flutter_libs: dependency: "direct overridden" description: path: "../../packages/powersync_flutter_libs" relative: true source: path - version: "0.4.2" + version: "0.4.10" pub_semver: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" realtime_client: dependency: transitive description: name: realtime_client - sha256: cd44fa21407a2e217d674f1c1a33b36c49ad0d8aea0349bf5b66594db06c80fb + sha256: "3a0a99b5bd0fc3b35e8ee846d9a22fa2c2117f7ef1cb73d1e5f08f6c3d09c4e9" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.5.0" retry: dependency: transitive description: @@ -522,79 +513,79 @@ packages: dependency: transitive description: name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" url: "https://pub.dev" source: hosted - version: "0.27.7" + version: "0.28.0" shared_preferences: dependency: transitive description: name: shared_preferences - sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577" + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.4.10" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.5.4" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.3" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" sprintf: dependency: transitive description: @@ -607,114 +598,114 @@ packages: dependency: transitive description: name: sqlite3 - sha256: bb174b3ec2527f9c5f680f73a89af8149dd99782fbb56ea88ad0807c5638f2ed + sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e" url: "https://pub.dev" source: hosted - version: "2.4.7" + version: "2.7.5" sqlite3_flutter_libs: dependency: transitive description: name: sqlite3_flutter_libs - sha256: "62bbb4073edbcdf53f40c80775f33eea01d301b7b81417e5b3fb7395416258c1" + sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14" url: "https://pub.dev" source: hosted - version: "0.5.24" + version: "0.5.32" sqlite3_web: dependency: transitive description: name: sqlite3_web - sha256: f22d1dda7a40be0867984f55cdf5c2d599e5f05d3be4a642d78f38b38983f554 + sha256: "967e076442f7e1233bd7241ca61f3efe4c7fc168dac0f38411bdb3bdf471eb3c" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.3.1" sqlite_async: dependency: "direct main" description: name: sqlite_async - sha256: d66fb6e6d07c1a834743326c033029f75becbb1fad6823d709f921872abc3d5b + sha256: a60e8d5c8df8e694933bd5a312c38393e79ad77d784bb91c6f38ba627bfb7aec url: "https://pub.dev" source: hosted - version: "0.11.0" + version: "0.11.4" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" storage_client: dependency: transitive description: name: storage_client - sha256: e37f1b9d40f43078d12bd2d1b6b08c2c16fbdbafc58b57bc44922da6ea3f5625 + sha256: "09bac4d75eea58e8113ca928e6655a09cc8059e6d1b472ee801f01fde815bcfc" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.4.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" supabase: dependency: transitive description: name: supabase - sha256: "073aabf6a9f6ada2ebb77082222e1104949afb9f7f181017d0643d99bda0efe3" + sha256: f00172f5f0b2148ea1c573f52862d50cacb6f353f579f741fa35e51704845958 url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.7.0" supabase_flutter: dependency: "direct main" description: name: supabase_flutter - sha256: ae56c20924fadd62f0a83f0570c22cec85f4b093768fbd0e049c4e1741a109a7 + sha256: d88eccf9e46e57129725a08e72a3109b6f780921fdc27fe3d7669a11ae80906b url: "https://pub.dev" source: hosted - version: "2.5.6" + version: "2.9.0" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.4" typed_data: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" universal_io: dependency: "direct main" description: @@ -727,42 +718,42 @@ packages: dependency: transitive description: name: url_launcher - sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" url: "https://pub.dev" source: hosted - version: "6.3.3" + version: "6.3.16" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89" + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.3" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: @@ -775,26 +766,26 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.1" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.4" uuid: dependency: transitive description: name: uuid - sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.5.1" vector_math: dependency: transitive description: @@ -807,42 +798,42 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "15.0.0" web: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" - web_socket_channel: + version: "1.1.1" + web_socket: dependency: transitive description: - name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "2.4.0" - win32: + version: "1.0.1" + web_socket_channel: dependency: transitive description: - name: win32 - sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "5.5.1" + version: "3.0.3" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: @@ -855,18 +846,18 @@ packages: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" yet_another_json_isolate: dependency: transitive description: name: yet_another_json_isolate - sha256: e727502a2640d65b4b8a8a6cb48af9dd0cbe644ba4b3ee667c7f4afa0c1d6069 + sha256: fe45897501fa156ccefbfb9359c9462ce5dec092f05e8a56109db30be864f01e url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/demos/supabase-todolist-optional-sync/pubspec.yaml b/demos/supabase-todolist-optional-sync/pubspec.yaml index 27c10b42..477f3d02 100644 --- a/demos/supabase-todolist-optional-sync/pubspec.yaml +++ b/demos/supabase-todolist-optional-sync/pubspec.yaml @@ -10,7 +10,7 @@ environment: dependencies: flutter: sdk: flutter - powersync: ^1.9.3 + powersync: ^1.16.1 path_provider: ^2.1.1 supabase_flutter: ^2.0.1 path: ^1.8.3 @@ -18,7 +18,7 @@ dependencies: camera: ^0.10.5+7 image: ^4.1.3 universal_io: ^2.2.2 - sqlite_async: ^0.11.0 + sqlite_async: ^0.12.0 dev_dependencies: flutter_test: diff --git a/demos/supabase-todolist/.gitignore b/demos/supabase-todolist/.gitignore index 1a825b5b..0f3655d3 100644 --- a/demos/supabase-todolist/.gitignore +++ b/demos/supabase-todolist/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related diff --git a/demos/supabase-todolist/.metadata b/demos/supabase-todolist/.metadata new file mode 100644 index 00000000..6a623a4e --- /dev/null +++ b/demos/supabase-todolist/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "d7b523b356d15fb81e7d340bbe52b47f93937323" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: android + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: ios + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: linux + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: macos + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: web + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: windows + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/demos/supabase-todolist/README.md b/demos/supabase-todolist/README.md index 542aa14d..55241dde 100644 --- a/demos/supabase-todolist/README.md +++ b/demos/supabase-todolist/README.md @@ -29,6 +29,26 @@ Create a new PowerSync instance, connecting to the database of the Supabase proj Then deploy the following sync rules: +```yaml +bucket_definitions: + user_lists: + priority: 1 + parameters: select id as list_id from lists where owner_id = request.user_id() + data: + - select * from lists where id = bucket.list_id + + user_todos: + parameters: select id as list_id from lists where owner_id = request.user_id() + data: + - select * from todos where list_id = bucket.list_id +``` + +**Note**: These rules showcase [prioritized sync](https://docs.powersync.com/usage/use-case-examples/prioritized-sync), +by syncing a user's lists with a higher priority than the items within a list (todos). This can be +useful to keep the list overview page reactive during a large sync cycle affecting many +rows in the `user_todos` bucket. The two buckets can also be unified into a single one if +priorities are not important (the app will work without changes): + ```yaml bucket_definitions: user_lists: diff --git a/demos/supabase-todolist/android/.gitignore b/demos/supabase-todolist/android/.gitignore index 6f568019..be3943c9 100644 --- a/demos/supabase-todolist/android/.gitignore +++ b/demos/supabase-todolist/android/.gitignore @@ -5,9 +5,10 @@ gradle-wrapper.jar /gradlew.bat /local.properties GeneratedPluginRegistrant.java +.cxx/ # Remember to never publicly share your keystore. -# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +# See https://flutter.dev/to/reference-keystore key.properties **/*.keystore **/*.jks diff --git a/demos/supabase-todolist/android/app/build.gradle.kts b/demos/supabase-todolist/android/app/build.gradle.kts new file mode 100644 index 00000000..2fc63bb4 --- /dev/null +++ b/demos/supabase-todolist/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.powersync.demo.flutter.supabase_todolist.powersync_flutter_demo" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.powersync.demo.flutter.supabase_todolist.powersync_flutter_demo" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/demos/supabase-todolist/android/app/src/debug/AndroidManifest.xml b/demos/supabase-todolist/android/app/src/debug/AndroidManifest.xml index f19dd7d6..399f6981 100644 --- a/demos/supabase-todolist/android/app/src/debug/AndroidManifest.xml +++ b/demos/supabase-todolist/android/app/src/debug/AndroidManifest.xml @@ -1,4 +1,7 @@ - + + diff --git a/demos/supabase-todolist/android/app/src/main/AndroidManifest.xml b/demos/supabase-todolist/android/app/src/main/AndroidManifest.xml index 55e175c4..e85f03e3 100644 --- a/demos/supabase-todolist/android/app/src/main/AndroidManifest.xml +++ b/demos/supabase-todolist/android/app/src/main/AndroidManifest.xml @@ -1,13 +1,13 @@ - - + + + + + + + + diff --git a/demos/supabase-todolist/android/app/src/main/kotlin/com/powersync/demo/flutter/supabase_todolist/powersync_flutter_demo/MainActivity.kt b/demos/supabase-todolist/android/app/src/main/kotlin/com/powersync/demo/flutter/supabase_todolist/powersync_flutter_demo/MainActivity.kt new file mode 100644 index 00000000..70c9dd12 --- /dev/null +++ b/demos/supabase-todolist/android/app/src/main/kotlin/com/powersync/demo/flutter/supabase_todolist/powersync_flutter_demo/MainActivity.kt @@ -0,0 +1,5 @@ +package com.powersync.demo.flutter.supabase_todolist.powersync_flutter_demo + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/demos/supabase-todolist/android/app/src/profile/AndroidManifest.xml b/demos/supabase-todolist/android/app/src/profile/AndroidManifest.xml index f19dd7d6..399f6981 100644 --- a/demos/supabase-todolist/android/app/src/profile/AndroidManifest.xml +++ b/demos/supabase-todolist/android/app/src/profile/AndroidManifest.xml @@ -1,4 +1,7 @@ - + + diff --git a/demos/supabase-todolist/android/build.gradle.kts b/demos/supabase-todolist/android/build.gradle.kts new file mode 100644 index 00000000..89176ef4 --- /dev/null +++ b/demos/supabase-todolist/android/build.gradle.kts @@ -0,0 +1,21 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/demos/supabase-todolist/android/gradle.properties b/demos/supabase-todolist/android/gradle.properties index 94adc3a3..f018a618 100644 --- a/demos/supabase-todolist/android/gradle.properties +++ b/demos/supabase-todolist/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true diff --git a/demos/supabase-todolist/android/gradle/wrapper/gradle-wrapper.properties b/demos/supabase-todolist/android/gradle/wrapper/gradle-wrapper.properties index 3c472b99..ac3b4792 100644 --- a/demos/supabase-todolist/android/gradle/wrapper/gradle-wrapper.properties +++ b/demos/supabase-todolist/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/demos/supabase-todolist/android/settings.gradle.kts b/demos/supabase-todolist/android/settings.gradle.kts new file mode 100644 index 00000000..ab39a10a --- /dev/null +++ b/demos/supabase-todolist/android/settings.gradle.kts @@ -0,0 +1,25 @@ +pluginManagement { + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.7.3" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false +} + +include(":app") diff --git a/demos/supabase-todolist/ios/Flutter/AppFrameworkInfo.plist b/demos/supabase-todolist/ios/Flutter/AppFrameworkInfo.plist index 7c569640..1dc6cf76 100644 --- a/demos/supabase-todolist/ios/Flutter/AppFrameworkInfo.plist +++ b/demos/supabase-todolist/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/demos/supabase-todolist/ios/Podfile b/demos/supabase-todolist/ios/Podfile index e9f73048..2c1e086a 100644 --- a/demos/supabase-todolist/ios/Podfile +++ b/demos/supabase-todolist/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '12.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/demos/supabase-todolist/ios/Podfile.lock b/demos/supabase-todolist/ios/Podfile.lock index abaa085c..73dadf31 100644 --- a/demos/supabase-todolist/ios/Podfile.lock +++ b/demos/supabase-todolist/ios/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - app_links (0.0.2): + - app_links (6.4.1): - Flutter - camera_avfoundation (0.0.1): - Flutter @@ -7,31 +7,39 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - powersync-sqlite-core (0.3.4) + - powersync-sqlite-core (0.4.6) - powersync_flutter_libs (0.0.1): - Flutter - - powersync-sqlite-core (~> 0.3.4) + - FlutterMacOS + - powersync-sqlite-core (~> 0.4.6) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - "sqlite3 (3.46.1+1)": - - "sqlite3/common (= 3.46.1+1)" - - "sqlite3/common (3.46.1+1)" - - "sqlite3/dbstatvtab (3.46.1+1)": + - sqlite3 (3.50.4): + - sqlite3/common (= 3.50.4) + - sqlite3/common (3.50.4) + - sqlite3/dbstatvtab (3.50.4): + - sqlite3/common + - sqlite3/fts5 (3.50.4): - sqlite3/common - - "sqlite3/fts5 (3.46.1+1)": + - sqlite3/math (3.50.4): - sqlite3/common - - "sqlite3/perf-threadsafe (3.46.1+1)": + - sqlite3/perf-threadsafe (3.50.4): - sqlite3/common - - "sqlite3/rtree (3.46.1+1)": + - sqlite3/rtree (3.50.4): + - sqlite3/common + - sqlite3/session (3.50.4): - sqlite3/common - sqlite3_flutter_libs (0.0.1): - Flutter - - "sqlite3 (~> 3.46.0+1)" + - FlutterMacOS + - sqlite3 (~> 3.50.4) - sqlite3/dbstatvtab - sqlite3/fts5 + - sqlite3/math - sqlite3/perf-threadsafe - sqlite3/rtree + - sqlite3/session - url_launcher_ios (0.0.1): - Flutter @@ -40,9 +48,9 @@ DEPENDENCIES: - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) - Flutter (from `Flutter`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - - powersync_flutter_libs (from `.symlinks/plugins/powersync_flutter_libs/ios`) + - powersync_flutter_libs (from `.symlinks/plugins/powersync_flutter_libs/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: @@ -60,26 +68,26 @@ EXTERNAL SOURCES: path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" powersync_flutter_libs: - :path: ".symlinks/plugins/powersync_flutter_libs/ios" + :path: ".symlinks/plugins/powersync_flutter_libs/darwin" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqlite3_flutter_libs: - :path: ".symlinks/plugins/sqlite3_flutter_libs/ios" + :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: - app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0 - camera_avfoundation: dd002b0330f4981e1bbcb46ae9b62829237459a4 - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - powersync-sqlite-core: d029aa444d33acbb05b47f9f9757b2650578e2d3 - powersync_flutter_libs: 29f1743509e9ada649dd38a365b3728b97d38c8b - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb - sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b - url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a + camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + powersync-sqlite-core: 42c4a42a692b3b770a5488778789430d67a39b49 + powersync_flutter_libs: 19fc6b96ff8155ffea72a08990f6c9f2e712b8a6 + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b + sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d -PODFILE CHECKSUM: f7b3cb7384a2d5da4b22b090e1f632de7f377987 +PODFILE CHECKSUM: 2c1730c97ea13f1ea48b32e9c79de785b4f2f02f -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/demos/supabase-todolist/ios/Runner.xcodeproj/project.pbxproj b/demos/supabase-todolist/ios/Runner.xcodeproj/project.pbxproj index 16636b7a..f6a8fba5 100644 --- a/demos/supabase-todolist/ios/Runner.xcodeproj/project.pbxproj +++ b/demos/supabase-todolist/ios/Runner.xcodeproj/project.pbxproj @@ -13,7 +13,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - B2C70762C97CE3E3CEB912CB /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B9CC0EA1BA15CD3CCAD0356 /* Pods_Runner.framework */; }; + FE22D026B50D91C63EC1E548 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3246A2C54ACF47297A0D9A97 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -32,13 +32,13 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 15764CEB058B2B69D5E35280 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - 3153F415177CAE497AE7D235 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 1AC6D6834A180EC866A1A907 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 3246A2C54ACF47297A0D9A97 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7B9CC0EA1BA15CD3CCAD0356 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7F544BD3701C5CF77F2FF87F /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -46,7 +46,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - CDF8C9971FE1B0CF3262ED53 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + F866E203CA1A8E0C6D9ABA5C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -54,13 +54,21 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - B2C70762C97CE3E3CEB912CB /* Pods_Runner.framework in Frameworks */, + FE22D026B50D91C63EC1E548 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 114FDDDC75E03531AE956759 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 3246A2C54ACF47297A0D9A97 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -79,7 +87,7 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, A151B04DC3D1415EEF784588 /* Pods */, - C1E97B63847FB6B811E12FEA /* Frameworks */, + 114FDDDC75E03531AE956759 /* Frameworks */, ); sourceTree = ""; }; @@ -109,21 +117,13 @@ A151B04DC3D1415EEF784588 /* Pods */ = { isa = PBXGroup; children = ( - 3153F415177CAE497AE7D235 /* Pods-Runner.debug.xcconfig */, - CDF8C9971FE1B0CF3262ED53 /* Pods-Runner.release.xcconfig */, - 15764CEB058B2B69D5E35280 /* Pods-Runner.profile.xcconfig */, + F866E203CA1A8E0C6D9ABA5C /* Pods-Runner.debug.xcconfig */, + 1AC6D6834A180EC866A1A907 /* Pods-Runner.release.xcconfig */, + 7F544BD3701C5CF77F2FF87F /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; }; - C1E97B63847FB6B811E12FEA /* Frameworks */ = { - isa = PBXGroup; - children = ( - 7B9CC0EA1BA15CD3CCAD0356 /* Pods_Runner.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -131,14 +131,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - E916CBFE94483EF7C2F17F6C /* [CP] Check Pods Manifest.lock */, + 39B3199DCFC6D38A9384399C /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 0A5FBCADCBC1AF2E0353A84D /* [CP] Embed Pods Frameworks */, + FE8EB41334261949D37FC328 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -197,21 +197,26 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 0A5FBCADCBC1AF2E0353A84D /* [CP] Embed Pods Frameworks */ = { + 39B3199DCFC6D38A9384399C /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Embed Pods Frameworks"; + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { @@ -245,26 +250,21 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - E916CBFE94483EF7C2F17F6C /* [CP] Check Pods Manifest.lock */ = { + FE8EB41334261949D37FC328 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -342,7 +342,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -419,7 +419,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -468,7 +468,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/demos/supabase-todolist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demos/supabase-todolist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5e31d3d3..9c12df59 100644 --- a/demos/supabase-todolist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/demos/supabase-todolist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> diff --git a/demos/supabase-todolist/ios/RunnerTests/RunnerTests.swift b/demos/supabase-todolist/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..86a7c3b1 --- /dev/null +++ b/demos/supabase-todolist/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/demos/supabase-todolist/lib/app_config_template.dart b/demos/supabase-todolist/lib/app_config_template.dart index ccfdfa21..05ea5164 100644 --- a/demos/supabase-todolist/lib/app_config_template.dart +++ b/demos/supabase-todolist/lib/app_config_template.dart @@ -6,4 +6,7 @@ class AppConfig { static const String powersyncUrl = 'https://foo.powersync.journeyapps.com'; static const String supabaseStorageBucket = ''; // Optional. Only required when syncing attachments and using Supabase Storage. See packages/powersync_attachments_helper. + // Whether the PowerSync instance uses sync streams to make fetching todo + // items optional. + static const bool hasSyncStreams = false; } diff --git a/demos/supabase-todolist/lib/attachments/local_storage_native.dart b/demos/supabase-todolist/lib/attachments/local_storage_native.dart new file mode 100644 index 00000000..31c35b96 --- /dev/null +++ b/demos/supabase-todolist/lib/attachments/local_storage_native.dart @@ -0,0 +1,8 @@ +import 'package:path_provider/path_provider.dart'; +import 'package:powersync_core/attachments/attachments.dart'; +import 'package:powersync_core/attachments/io.dart'; + +Future localAttachmentStorage() async { + final appDocDir = await getApplicationDocumentsDirectory(); + return IOLocalStorage(appDocDir); +} diff --git a/demos/supabase-todolist/lib/attachments/local_storage_unsupported.dart b/demos/supabase-todolist/lib/attachments/local_storage_unsupported.dart new file mode 100644 index 00000000..811fa7d3 --- /dev/null +++ b/demos/supabase-todolist/lib/attachments/local_storage_unsupported.dart @@ -0,0 +1,7 @@ +import 'package:powersync_core/attachments/attachments.dart'; + +Future localAttachmentStorage() async { + // This file is imported on the web, where we don't currently have a + // persistent local storage implementation. + return LocalStorage.inMemory(); +} diff --git a/demos/supabase-todolist/lib/attachments/photo_capture_widget.dart b/demos/supabase-todolist/lib/attachments/photo_capture_widget.dart index 38838dd7..89660808 100644 --- a/demos/supabase-todolist/lib/attachments/photo_capture_widget.dart +++ b/demos/supabase-todolist/lib/attachments/photo_capture_widget.dart @@ -1,11 +1,9 @@ import 'dart:async'; - +import 'dart:io'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; -import 'package:powersync/powersync.dart' as powersync; +import 'package:logging/logging.dart'; import 'package:powersync_flutter_demo/attachments/queue.dart'; -import 'package:powersync_flutter_demo/models/todo_item.dart'; -import 'package:powersync_flutter_demo/powersync.dart'; class TakePhotoWidget extends StatefulWidget { final String todoId; @@ -23,6 +21,7 @@ class TakePhotoWidget extends StatefulWidget { class _TakePhotoWidgetState extends State { late CameraController _cameraController; late Future _initializeControllerFuture; + final log = Logger('TakePhotoWidget'); @override void initState() { @@ -37,7 +36,6 @@ class _TakePhotoWidgetState extends State { } @override - // Dispose of the camera controller when the widget is disposed void dispose() { _cameraController.dispose(); super.dispose(); @@ -45,25 +43,26 @@ class _TakePhotoWidgetState extends State { Future _takePhoto(context) async { try { - // Ensure the camera is initialized before taking a photo + log.info('Taking photo for todo: ${widget.todoId}'); await _initializeControllerFuture; - final XFile photo = await _cameraController.takePicture(); - // copy photo to new directory with ID as name - String photoId = powersync.uuid.v4(); - String storageDirectory = await attachmentQueue.getStorageDirectory(); - await attachmentQueue.localStorage - .copyFile(photo.path, '$storageDirectory/$photoId.jpg'); - int photoSize = await photo.length(); + // Read the photo data as bytes + final photoFile = File(photo.path); + if (!await photoFile.exists()) { + log.warning('Photo file does not exist: ${photo.path}'); + return; + } + + final photoData = photoFile.openRead(); - TodoItem.addPhoto(photoId, widget.todoId); - attachmentQueue.saveFile(photoId, photoSize); + // Save the photo attachment with the byte data + final attachment = await savePhotoAttachment(photoData, widget.todoId); + + log.info('Photo attachment saved with ID: ${attachment.id}'); } catch (e) { - log.info('Error taking photo: $e'); + log.severe('Error taking photo: $e'); } - - // After taking the photo, navigate back to the previous screen Navigator.pop(context); } diff --git a/demos/supabase-todolist/lib/attachments/photo_widget.dart b/demos/supabase-todolist/lib/attachments/photo_widget.dart index f034ef5b..f41bc0b0 100644 --- a/demos/supabase-todolist/lib/attachments/photo_widget.dart +++ b/demos/supabase-todolist/lib/attachments/photo_widget.dart @@ -1,12 +1,14 @@ import 'dart:io'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as p; import 'package:flutter/material.dart'; -import 'package:powersync_attachments_helper/powersync_attachments_helper.dart'; +import 'package:powersync_core/attachments/attachments.dart'; import 'package:powersync_flutter_demo/attachments/camera_helpers.dart'; import 'package:powersync_flutter_demo/attachments/photo_capture_widget.dart'; -import 'package:powersync_flutter_demo/attachments/queue.dart'; import '../models/todo_item.dart'; +import '../powersync.dart'; class PhotoWidget extends StatefulWidget { final TodoItem todo; @@ -37,11 +39,12 @@ class _PhotoWidgetState extends State { if (photoId == null) { return _ResolvedPhotoState(photoPath: null, fileExists: false); } - photoPath = await attachmentQueue.getLocalUri('$photoId.jpg'); + final appDocDir = await getApplicationDocumentsDirectory(); + photoPath = p.join(appDocDir.path, '$photoId.jpg'); bool fileExists = await File(photoPath).exists(); - final row = await attachmentQueue.db + final row = await db .getOptional('SELECT * FROM attachments_queue WHERE id = ?', [photoId]); if (row != null) { @@ -98,7 +101,7 @@ class _PhotoWidgetState extends State { String? filePath = data.photoPath; bool fileIsDownloading = !data.fileExists; bool fileArchived = - data.attachment?.state == AttachmentState.archived.index; + data.attachment?.state == AttachmentState.archived; if (fileArchived) { return Column( diff --git a/demos/supabase-todolist/lib/attachments/queue.dart b/demos/supabase-todolist/lib/attachments/queue.dart index 2a8dd9ca..8f036c85 100644 --- a/demos/supabase-todolist/lib/attachments/queue.dart +++ b/demos/supabase-todolist/lib/attachments/queue.dart @@ -1,90 +1,55 @@ import 'dart:async'; +import 'package:logging/logging.dart'; import 'package:powersync/powersync.dart'; -import 'package:powersync_attachments_helper/powersync_attachments_helper.dart'; -import 'package:powersync_flutter_demo/app_config.dart'; +import 'package:powersync_core/attachments/attachments.dart'; + import 'package:powersync_flutter_demo/attachments/remote_storage_adapter.dart'; -import 'package:powersync_flutter_demo/models/schema.dart'; +import 'local_storage_unsupported.dart' + if (dart.library.io) 'local_storage_native.dart'; -/// Global reference to the queue -late final PhotoAttachmentQueue attachmentQueue; +late AttachmentQueue attachmentQueue; final remoteStorage = SupabaseStorageAdapter(); - -/// Function to handle errors when downloading attachments -/// Return false if you want to archive the attachment -Future onDownloadError(Attachment attachment, Object exception) async { - if (exception.toString().contains('Object not found')) { - return false; - } - return true; -} - -class PhotoAttachmentQueue extends AbstractAttachmentQueue { - PhotoAttachmentQueue(db, remoteStorage) - : super( - db: db, - remoteStorage: remoteStorage, - onDownloadError: onDownloadError); - - @override - init() async { - if (AppConfig.supabaseStorageBucket.isEmpty) { - log.info( - 'No Supabase bucket configured, skip setting up PhotoAttachmentQueue watches'); - return; - } - - await super.init(); - } - - @override - Future saveFile(String fileId, int size, - {mediaType = 'image/jpeg'}) async { - String filename = '$fileId.jpg'; - - Attachment photoAttachment = Attachment( - id: fileId, - filename: filename, - state: AttachmentState.queuedUpload.index, - mediaType: mediaType, - localUri: getLocalFilePathSuffix(filename), - size: size, - ); - - return attachmentsService.saveAttachment(photoAttachment); - } - - @override - Future deleteFile(String fileId) async { - String filename = '$fileId.jpg'; - - Attachment photoAttachment = Attachment( - id: fileId, - filename: filename, - state: AttachmentState.queuedDelete.index); - - return attachmentsService.saveAttachment(photoAttachment); - } - - @override - StreamSubscription watchIds({String fileExtension = 'jpg'}) { - log.info('Watching photos in $todosTable...'); - return db.watch(''' - SELECT photo_id FROM $todosTable - WHERE photo_id IS NOT NULL - ''').map((results) { - return results.map((row) => row['photo_id'] as String).toList(); - }).listen((ids) async { - List idsInQueue = await attachmentsService.getAttachmentIds(); - List relevantIds = - ids.where((element) => !idsInQueue.contains(element)).toList(); - syncingService.processIds(relevantIds, fileExtension); - }); - } +final logger = Logger('AttachmentQueue'); + +Future initializeAttachmentQueue(PowerSyncDatabase db) async { + attachmentQueue = AttachmentQueue( + db: db, + remoteStorage: remoteStorage, + logger: logger, + localStorage: await localAttachmentStorage(), + watchAttachments: () => db.watch(''' + SELECT photo_id as id FROM todos WHERE photo_id IS NOT NULL + ''').map( + (results) => [ + for (final row in results) + WatchedAttachmentItem( + id: row['id'] as String, + fileExtension: 'jpg', + ) + ], + ), + ); + + await attachmentQueue.startSync(); } -initializeAttachmentQueue(PowerSyncDatabase db) async { - attachmentQueue = PhotoAttachmentQueue(db, remoteStorage); - await attachmentQueue.init(); +Future savePhotoAttachment( + Stream> photoData, String todoId, + {String mediaType = 'image/jpeg'}) async { + // Save the file using the AttachmentQueue API + return await attachmentQueue.saveFile( + data: photoData, + mediaType: mediaType, + fileExtension: 'jpg', + metaData: 'Photo attachment for todo: $todoId', + updateHook: (context, attachment) async { + // Update the todo item to reference this attachment + await context.execute( + 'UPDATE todos SET photo_id = ? WHERE id = ?', + [attachment.id, todoId], + ); + }, + ); } diff --git a/demos/supabase-todolist/lib/attachments/remote_storage_adapter.dart b/demos/supabase-todolist/lib/attachments/remote_storage_adapter.dart index 596c5da5..5b711b83 100644 --- a/demos/supabase-todolist/lib/attachments/remote_storage_adapter.dart +++ b/demos/supabase-todolist/lib/attachments/remote_storage_adapter.dart @@ -1,49 +1,96 @@ import 'dart:io'; import 'dart:typed_data'; -import 'package:powersync_attachments_helper/powersync_attachments_helper.dart'; + +import 'package:powersync_core/attachments/attachments.dart'; import 'package:powersync_flutter_demo/app_config.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; -import 'package:image/image.dart' as img; +import 'package:logging/logging.dart'; + +class SupabaseStorageAdapter implements RemoteStorage { + static final _log = Logger('SupabaseStorageAdapter'); -class SupabaseStorageAdapter implements AbstractRemoteStorageAdapter { @override - Future uploadFile(String filename, File file, - {String mediaType = 'text/plain'}) async { + Future uploadFile( + Stream> fileData, Attachment attachment) async { _checkSupabaseBucketIsConfigured(); + // Check if attachment size is specified (required for buffer allocation) + final byteSize = attachment.size; + if (byteSize == null) { + throw Exception('Cannot upload a file with no byte size specified'); + } + + _log.info('uploadFile: ${attachment.filename} (size: $byteSize bytes)'); + + // Collect all stream data into a single Uint8List buffer + final buffer = Uint8List(byteSize); + var position = 0; + + await for (final chunk in fileData) { + if (position + chunk.length > byteSize) { + throw Exception('File data exceeds specified size'); + } + buffer.setRange(position, position + chunk.length, chunk); + position += chunk.length; + } + + if (position != byteSize) { + throw Exception( + 'File data size ($position) does not match specified size ($byteSize)'); + } + + // Create a temporary file from the buffer for upload + final tempFile = + File('${Directory.systemTemp.path}/${attachment.filename}'); try { + await tempFile.writeAsBytes(buffer); + await Supabase.instance.client.storage .from(AppConfig.supabaseStorageBucket) - .upload(filename, file, - fileOptions: FileOptions(contentType: mediaType)); + .upload(attachment.filename, tempFile, + fileOptions: FileOptions( + contentType: + attachment.mediaType ?? 'application/octet-stream')); + + _log.info('Successfully uploaded ${attachment.filename}'); } catch (error) { + _log.severe('Error uploading ${attachment.filename}', error); throw Exception(error); + } finally { + if (await tempFile.exists()) { + await tempFile.delete(); + } } } @override - Future downloadFile(String filePath) async { + Future>> downloadFile(Attachment attachment) async { _checkSupabaseBucketIsConfigured(); try { + _log.info('downloadFile: ${attachment.filename}'); + Uint8List fileBlob = await Supabase.instance.client.storage .from(AppConfig.supabaseStorageBucket) - .download(filePath); - final image = img.decodeImage(fileBlob); - Uint8List blob = img.JpegEncoder().encode(image!); - return blob; + .download(attachment.filename); + + _log.info( + 'Successfully downloaded ${attachment.filename} (${fileBlob.length} bytes)'); + + // Return the raw file data as a stream + return Stream.value(fileBlob); } catch (error) { + _log.severe('Error downloading ${attachment.filename}', error); throw Exception(error); } } @override - Future deleteFile(String filename) async { + Future deleteFile(Attachment attachment) async { _checkSupabaseBucketIsConfigured(); - try { await Supabase.instance.client.storage .from(AppConfig.supabaseStorageBucket) - .remove([filename]); + .remove([attachment.filename]); } catch (error) { throw Exception(error); } diff --git a/demos/supabase-todolist/lib/main.dart b/demos/supabase-todolist/lib/main.dart index f45f2b21..7f9dac55 100644 --- a/demos/supabase-todolist/lib/main.dart +++ b/demos/supabase-todolist/lib/main.dart @@ -46,7 +46,7 @@ const listsPage = ListsPage(); const homePage = listsPage; const sqlConsolePage = Scaffold( - appBar: StatusAppBar(title: 'SQL Console'), + appBar: StatusAppBar(title: Text('SQL Console')), body: QueryWidget(defaultQuery: defaultQuery)); const loginPage = LoginPage(); @@ -83,7 +83,7 @@ class MyHomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: StatusAppBar(title: title), + appBar: StatusAppBar(title: Text(title)), body: Center(child: content), floatingActionButton: floatingActionButton, drawer: Drawer( diff --git a/demos/supabase-todolist/lib/models/schema.dart b/demos/supabase-todolist/lib/models/schema.dart index 89b69b0c..5a6a261b 100644 --- a/demos/supabase-todolist/lib/models/schema.dart +++ b/demos/supabase-todolist/lib/models/schema.dart @@ -1,9 +1,9 @@ import 'package:powersync/powersync.dart'; -import 'package:powersync_attachments_helper/powersync_attachments_helper.dart'; +import 'package:powersync_core/attachments/attachments.dart'; const todosTable = 'todos'; -Schema schema = Schema(([ +Schema schema = Schema([ const Table(todosTable, [ Column.text('list_id'), Column.text('photo_id'), @@ -22,6 +22,5 @@ Schema schema = Schema(([ Column.text('name'), Column.text('owner_id') ]), - AttachmentsQueueTable( - attachmentsQueueTableName: defaultAttachmentsQueueTableName) -])); + AttachmentsQueueTable() +]); diff --git a/demos/supabase-todolist/lib/powersync.dart b/demos/supabase-todolist/lib/powersync.dart index 166de1b3..412e53c7 100644 --- a/demos/supabase-todolist/lib/powersync.dart +++ b/demos/supabase-todolist/lib/powersync.dart @@ -153,6 +153,8 @@ Future getDatabasePath() async { return join(dir.path, dbFilename); } +const options = SyncOptions(syncImplementation: SyncClientImplementation.rust); + Future openDatabase() async { // Open the local database db = PowerSyncDatabase( @@ -167,7 +169,7 @@ Future openDatabase() async { // If the user is already logged in, connect immediately. // Otherwise, connect once logged in. currentConnector = SupabaseConnector(); - db.connect(connector: currentConnector); + db.connect(connector: currentConnector, options: options); } Supabase.instance.client.auth.onAuthStateChange.listen((data) async { @@ -175,7 +177,7 @@ Future openDatabase() async { if (event == AuthChangeEvent.signedIn) { // Connect to PowerSync when the user is signed in currentConnector = SupabaseConnector(); - db.connect(connector: currentConnector!); + db.connect(connector: currentConnector!, options: options); } else if (event == AuthChangeEvent.signedOut) { // Implicit sign out - disconnect, but don't delete data currentConnector = null; diff --git a/demos/supabase-todolist/lib/widgets/guard_by_sync.dart b/demos/supabase-todolist/lib/widgets/guard_by_sync.dart new file mode 100644 index 00000000..6d4d12b9 --- /dev/null +++ b/demos/supabase-todolist/lib/widgets/guard_by_sync.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:powersync/powersync.dart' hide Column; +import 'package:powersync_flutter_demo/powersync.dart'; + +/// A widget that shows [child] after a complete sync on the database has +/// completed and a progress bar before that. +class GuardBySync extends StatelessWidget { + final Widget child; + + /// When set, wait only for a complete sync within the [StreamPriority] + /// instead of a full sync. + final StreamPriority? priority; + + const GuardBySync({ + super.key, + required this.child, + this.priority, + }); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: db.statusStream, + initialData: db.currentStatus, + builder: (context, snapshot) { + final status = snapshot.requireData; + final (didSync, progress) = switch (priority) { + null => (status.hasSynced ?? false, status.downloadProgress), + var priority? => ( + status.statusForPriority(priority).hasSynced ?? false, + status.downloadProgress?.untilPriority(priority) + ), + }; + + if (didSync) { + return child; + } else { + return Center( + child: Column( + children: [ + const Text('Busy with sync...'), + LinearProgressIndicator(value: progress?.downloadedFraction), + if (progress case final progress?) + Text( + '${progress.downloadedOperations} out of ${progress.totalOperations}') + ], + ), + ); + } + }, + ); + } +} diff --git a/demos/supabase-todolist/lib/widgets/list_item_sync_stream.dart b/demos/supabase-todolist/lib/widgets/list_item_sync_stream.dart new file mode 100644 index 00000000..e400f8aa --- /dev/null +++ b/demos/supabase-todolist/lib/widgets/list_item_sync_stream.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; + +import '../powersync.dart'; +import './todo_list_page.dart'; +import '../models/todo_list.dart'; + +/// A variant of the `ListItem` that only shows a summary of completed and +/// pending items when the respective list has an active sync stream. +class SyncStreamsAwareListItem extends StatelessWidget { + SyncStreamsAwareListItem({ + required this.list, + }) : super(key: ObjectKey(list)); + + final TodoList list; + + Future delete() async { + // Server will take care of deleting related todos + await list.delete(); + } + + @override + Widget build(BuildContext context) { + viewList() { + var navigator = Navigator.of(context); + + navigator.push( + MaterialPageRoute(builder: (context) => TodoListPage(list: list))); + } + + return StreamBuilder( + stream: db.statusStream, + initialData: db.currentStatus, + builder: (context, asyncSnapshot) { + final status = asyncSnapshot.requireData; + final stream = + status.forStream(db.syncStream('todos', {'list': list.id})); + + String subtext; + if (stream == null || !stream.subscription.active) { + subtext = 'Items not loaded - click to fetch.'; + } else { + subtext = + '${list.pendingCount} pending, ${list.completedCount} completed'; + } + + return Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + onTap: viewList, + leading: const Icon(Icons.list), + title: Text(list.name), + subtitle: Text(subtext), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton( + iconSize: 30, + icon: const Icon( + Icons.delete, + color: Colors.red, + ), + tooltip: 'Delete List', + alignment: Alignment.centerRight, + onPressed: delete, + ), + const SizedBox(width: 8), + ], + ), + ], + ), + ); + }, + ); + } +} diff --git a/demos/supabase-todolist/lib/widgets/lists_page.dart b/demos/supabase-todolist/lib/widgets/lists_page.dart index 142d9e9f..564e03ee 100644 --- a/demos/supabase-todolist/lib/widgets/lists_page.dart +++ b/demos/supabase-todolist/lib/widgets/lists_page.dart @@ -1,11 +1,13 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; +import 'package:powersync/powersync.dart'; +import '../app_config.dart'; import './list_item.dart'; import './list_item_dialog.dart'; import '../main.dart'; import '../models/todo_list.dart'; +import 'guard_by_sync.dart'; +import 'list_item_sync_stream.dart'; void _showAddDialog(BuildContext context) async { return showDialog( @@ -41,61 +43,32 @@ class ListsPage extends StatelessWidget { } } -class ListsWidget extends StatefulWidget { +class ListsWidget extends StatelessWidget { const ListsWidget({super.key}); - @override - State createState() { - return _ListsWidgetState(); - } -} - -class _ListsWidgetState extends State { - List _data = []; - bool hasSynced = false; - StreamSubscription? _subscription; - StreamSubscription? _syncStatusSubscription; - - _ListsWidgetState(); - - @override - void initState() { - super.initState(); - final stream = TodoList.watchListsWithStats(); - _subscription = stream.listen((data) { - if (!context.mounted) { - return; - } - setState(() { - _data = data; - }); - }); - _syncStatusSubscription = TodoList.watchSyncStatus().listen((status) { - if (!context.mounted) { - return; - } - setState(() { - hasSynced = status.hasSynced ?? false; - }); - }); - } - - @override - void dispose() { - super.dispose(); - _subscription?.cancel(); - _syncStatusSubscription?.cancel(); - } - @override Widget build(BuildContext context) { - return !hasSynced - ? const Text("Busy with sync...") - : ListView( - padding: const EdgeInsets.symmetric(vertical: 8.0), - children: _data.map((list) { - return ListItemWidget(list: list); - }).toList(), - ); + return GuardBySync( + priority: _listsPriority, + child: StreamBuilder( + stream: TodoList.watchListsWithStats(), + builder: (context, snapshot) { + if (snapshot.data case final todoLists?) { + return ListView( + padding: const EdgeInsets.symmetric(vertical: 8.0), + children: todoLists.map((list) { + return AppConfig.hasSyncStreams + ? SyncStreamsAwareListItem(list: list) + : ListItemWidget(list: list); + }).toList(), + ); + } else { + return const CircularProgressIndicator(); + } + }, + ), + ); } + + static final _listsPriority = StreamPriority(1); } diff --git a/demos/supabase-todolist/lib/widgets/status_app_bar.dart b/demos/supabase-todolist/lib/widgets/status_app_bar.dart index 5722cc88..c3523d83 100644 --- a/demos/supabase-todolist/lib/widgets/status_app_bar.dart +++ b/demos/supabase-todolist/lib/widgets/status_app_bar.dart @@ -1,62 +1,42 @@ -import 'dart:async'; - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:powersync/powersync.dart'; import 'package:powersync_flutter_demo/widgets/fts_search_delegate.dart'; import '../powersync.dart'; -class StatusAppBar extends StatefulWidget implements PreferredSizeWidget { - const StatusAppBar({super.key, required this.title}); - - final String title; +class StatusAppBar extends StatelessWidget implements PreferredSizeWidget { + final Widget title; - @override - State createState() => _StatusAppBarState(); + const StatusAppBar({super.key, required this.title}); @override Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} - -class _StatusAppBarState extends State { - late SyncStatus _connectionState; - StreamSubscription? _syncStatusSubscription; - - @override - void initState() { - super.initState(); - _connectionState = db.currentStatus; - _syncStatusSubscription = db.statusStream.listen((event) { - setState(() { - _connectionState = db.currentStatus; - }); - }); - } - - @override - void dispose() { - super.dispose(); - _syncStatusSubscription?.cancel(); - } @override Widget build(BuildContext context) { - final statusIcon = _getStatusIcon(_connectionState); + return StreamBuilder( + stream: db.statusStream, + initialData: db.currentStatus, + builder: (context, snapshot) { + final status = snapshot.data!; + final statusIcon = _getStatusIcon(status); - return AppBar( - title: Text(widget.title), - actions: [ - IconButton( - onPressed: () { - showSearch(context: context, delegate: FtsSearchDelegate()); - }, - icon: const Icon(Icons.search), - ), - statusIcon, - // Make some space for the "Debug" banner, so that the status - // icon isn't hidden - if (kDebugMode) _makeIcon('Debug mode', Icons.developer_mode), - ], + return AppBar( + title: title, + actions: [ + IconButton( + onPressed: () { + showSearch(context: context, delegate: FtsSearchDelegate()); + }, + icon: const Icon(Icons.search), + ), + statusIcon, + // Make some space for the "Debug" banner, so that the status + // icon isn't hidden + if (kDebugMode) _makeIcon('Debug mode', Icons.developer_mode), + ], + ); + }, ); } } diff --git a/demos/supabase-todolist/lib/widgets/todo_item_widget.dart b/demos/supabase-todolist/lib/widgets/todo_item_widget.dart index a59812ed..700a869a 100644 --- a/demos/supabase-todolist/lib/widgets/todo_item_widget.dart +++ b/demos/supabase-todolist/lib/widgets/todo_item_widget.dart @@ -23,7 +23,13 @@ class TodoItemWidget extends StatelessWidget { Future deleteTodo(TodoItem todo) async { if (todo.photoId != null) { - attachmentQueue.deleteFile(todo.photoId!); + await attachmentQueue.deleteFile( + attachmentId: todo.photoId!, + updateHook: (context, attachment) async { + await context.execute( + "UPDATE todos SET photo_id = NULL WHERE id = ?", [todo.id]); + }, + ); } await todo.delete(); } diff --git a/demos/supabase-todolist/lib/widgets/todo_list_page.dart b/demos/supabase-todolist/lib/widgets/todo_list_page.dart index e36e1867..b1245321 100644 --- a/demos/supabase-todolist/lib/widgets/todo_list_page.dart +++ b/demos/supabase-todolist/lib/widgets/todo_list_page.dart @@ -1,8 +1,8 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; -import 'package:powersync_flutter_demo/models/todo_item.dart'; +import 'package:powersync/powersync.dart'; +import '../app_config.dart'; +import '../powersync.dart'; import './status_app_bar.dart'; import './todo_item_dialog.dart'; import './todo_item_widget.dart'; @@ -34,56 +34,121 @@ class TodoListPage extends StatelessWidget { ); return Scaffold( - appBar: StatusAppBar(title: list.name), - floatingActionButton: button, - body: TodoListWidget(list: list)); + appBar: StatusAppBar(title: Text(list.name)), + floatingActionButton: button, + body: AppConfig.hasSyncStreams + ? _SyncStreamTodoListWidget(list: list) + : TodoListWidget(list: list), + ); } } -class TodoListWidget extends StatefulWidget { +class TodoListWidget extends StatelessWidget { final TodoList list; const TodoListWidget({super.key, required this.list}); @override - State createState() { - return TodoListWidgetState(); + Widget build(BuildContext context) { + return StreamBuilder( + stream: TodoList.watchSyncStatus().map((e) => e.hasSynced), + initialData: db.currentStatus.hasSynced, + builder: (context, snapshot) { + return StreamBuilder( + stream: list.watchItems(), + builder: (context, snapshot) { + final items = snapshot.data ?? const []; + + return ListView( + padding: const EdgeInsets.symmetric(vertical: 8.0), + children: items.map((todo) { + return TodoItemWidget(todo: todo); + }).toList(), + ); + }, + ); + }, + ); } } -class TodoListWidgetState extends State { - List _data = []; - StreamSubscription? _subscription; +class _SyncStreamTodoListWidget extends StatefulWidget { + final TodoList list; - TodoListWidgetState(); + const _SyncStreamTodoListWidget({required this.list}); + + @override + State<_SyncStreamTodoListWidget> createState() => _SyncStreamTodosState(); +} + +class _SyncStreamTodosState extends State<_SyncStreamTodoListWidget> { + SyncStreamSubscription? _listSubscription; + + void _subscribe(String listId) { + db + .syncStream('todos', {'list': listId}) + .subscribe(ttl: const Duration(hours: 1)) + .then((sub) { + if (mounted && widget.list.id == listId) { + setState(() { + _listSubscription = sub; + }); + } else { + sub.unsubscribe(); + } + }); + } @override void initState() { super.initState(); - final stream = widget.list.watchItems(); - _subscription = stream.listen((data) { - if (!context.mounted) { - return; - } - setState(() { - _data = data; - }); - }); + _subscribe(widget.list.id); + } + + @override + void didUpdateWidget(covariant _SyncStreamTodoListWidget oldWidget) { + super.didUpdateWidget(oldWidget); + _subscribe(widget.list.id); } @override void dispose() { super.dispose(); - _subscription?.cancel(); + _listSubscription?.unsubscribe(); } @override Widget build(BuildContext context) { - return ListView( - padding: const EdgeInsets.symmetric(vertical: 8.0), - children: _data.map((todo) { - return TodoItemWidget(todo: todo); - }).toList(), + return StreamBuilder( + stream: db.statusStream, + initialData: db.currentStatus, + builder: (context, snapshot) { + final hasSynced = switch (_listSubscription) { + null => null, + final sub => snapshot.requireData.forStream(sub), + } + ?.subscription + .hasSynced ?? + false; + + if (!hasSynced) { + return const Center(child: CircularProgressIndicator()); + } else { + return StreamBuilder( + stream: widget.list.watchItems(), + builder: (context, snapshot) { + final items = snapshot.data ?? const []; + + return ListView( + padding: const EdgeInsets.symmetric(vertical: 8.0), + children: items.map((todo) { + return TodoItemWidget(todo: todo); + }).toList(), + ); + }, + ); + } + }, ); } } diff --git a/demos/supabase-todolist/linux/runner/CMakeLists.txt b/demos/supabase-todolist/linux/runner/CMakeLists.txt new file mode 100644 index 00000000..e97dabc7 --- /dev/null +++ b/demos/supabase-todolist/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/demos/supabase-todolist/linux/runner/main.cc b/demos/supabase-todolist/linux/runner/main.cc new file mode 100644 index 00000000..e7c5c543 --- /dev/null +++ b/demos/supabase-todolist/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/demos/supabase-todolist/linux/runner/my_application.cc b/demos/supabase-todolist/linux/runner/my_application.cc new file mode 100644 index 00000000..dde6b0ac --- /dev/null +++ b/demos/supabase-todolist/linux/runner/my_application.cc @@ -0,0 +1,130 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "powersync_flutter_demo"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "powersync_flutter_demo"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/demos/supabase-todolist/linux/runner/my_application.h b/demos/supabase-todolist/linux/runner/my_application.h new file mode 100644 index 00000000..72271d5e --- /dev/null +++ b/demos/supabase-todolist/linux/runner/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/demos/supabase-todolist/macos/Podfile b/demos/supabase-todolist/macos/Podfile index c795730d..b52666a1 100644 --- a/demos/supabase-todolist/macos/Podfile +++ b/demos/supabase-todolist/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.14' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/demos/supabase-todolist/macos/Podfile.lock b/demos/supabase-todolist/macos/Podfile.lock index cd6e215f..51b6fdad 100644 --- a/demos/supabase-todolist/macos/Podfile.lock +++ b/demos/supabase-todolist/macos/Podfile.lock @@ -1,35 +1,43 @@ PODS: - - app_links (1.0.0): + - app_links (6.4.1): - FlutterMacOS - FlutterMacOS (1.0.0) - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - powersync-sqlite-core (0.3.4) + - powersync-sqlite-core (0.4.6) - powersync_flutter_libs (0.0.1): + - Flutter - FlutterMacOS - - powersync-sqlite-core (~> 0.3.4) + - powersync-sqlite-core (~> 0.4.6) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - "sqlite3 (3.46.1+1)": - - "sqlite3/common (= 3.46.1+1)" - - "sqlite3/common (3.46.1+1)" - - "sqlite3/dbstatvtab (3.46.1+1)": + - sqlite3 (3.50.4): + - sqlite3/common (= 3.50.4) + - sqlite3/common (3.50.4) + - sqlite3/dbstatvtab (3.50.4): + - sqlite3/common + - sqlite3/fts5 (3.50.4): - sqlite3/common - - "sqlite3/fts5 (3.46.1+1)": + - sqlite3/math (3.50.4): - sqlite3/common - - "sqlite3/perf-threadsafe (3.46.1+1)": + - sqlite3/perf-threadsafe (3.50.4): - sqlite3/common - - "sqlite3/rtree (3.46.1+1)": + - sqlite3/rtree (3.50.4): + - sqlite3/common + - sqlite3/session (3.50.4): - sqlite3/common - sqlite3_flutter_libs (0.0.1): + - Flutter - FlutterMacOS - - "sqlite3 (~> 3.46.0+1)" + - sqlite3 (~> 3.50.4) - sqlite3/dbstatvtab - sqlite3/fts5 + - sqlite3/math - sqlite3/perf-threadsafe - sqlite3/rtree + - sqlite3/session - url_launcher_macos (0.0.1): - FlutterMacOS @@ -37,9 +45,9 @@ DEPENDENCIES: - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - - powersync_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/powersync_flutter_libs/macos`) + - powersync_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/powersync_flutter_libs/darwin`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`) + - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) SPEC REPOS: @@ -55,25 +63,25 @@ EXTERNAL SOURCES: path_provider_foundation: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin powersync_flutter_libs: - :path: Flutter/ephemeral/.symlinks/plugins/powersync_flutter_libs/macos + :path: Flutter/ephemeral/.symlinks/plugins/powersync_flutter_libs/darwin shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin sqlite3_flutter_libs: - :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos + :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: - app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a - FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - powersync-sqlite-core: d029aa444d33acbb05b47f9f9757b2650578e2d3 - powersync_flutter_libs: 44829eda70d4f87c9271e963a54126ce19408d7c - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb - sqlite3_flutter_libs: 5ca46c1a04eddfbeeb5b16566164aa7ad1616e7b - url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 + app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + powersync-sqlite-core: 42c4a42a692b3b770a5488778789430d67a39b49 + powersync_flutter_libs: 19fc6b96ff8155ffea72a08990f6c9f2e712b8a6 + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b + sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 -PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 +PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/demos/supabase-todolist/macos/Runner.xcodeproj/project.pbxproj b/demos/supabase-todolist/macos/Runner.xcodeproj/project.pbxproj index fe47ba76..8c3f956d 100644 --- a/demos/supabase-todolist/macos/Runner.xcodeproj/project.pbxproj +++ b/demos/supabase-todolist/macos/Runner.xcodeproj/project.pbxproj @@ -21,14 +21,14 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ - 04EE2EEA1AF4432FCFE4D947 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 386AF35B349F70B5D676F5EC /* Pods_Runner.framework */; }; - 2F56F886B3B1884D3E437FD0 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C2FC729F34600C40853A030B /* Pods_RunnerTests.framework */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 75E48BA0AEB945CF7281B8D7 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 610323F3498FEA93EE8D7ECD /* Pods_Runner.framework */; }; 8B5261612A7C463D00E9899E /* powersync_flutter_demoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B5261602A7C463D00E9899E /* powersync_flutter_demoTests.swift */; }; + 9381A48772266D9C49309994 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 991B2149AEB0B3DAB09CB3BE /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -62,7 +62,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 1FB90A99EA939D06EE287C09 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 10C22D07423926C20887DE48 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; 33CC10ED2044A3C60003C045 /* powersync_flutter_demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = powersync_flutter_demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -77,17 +77,17 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 386AF35B349F70B5D676F5EC /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4D425E3DE8C8153AB8C55A47 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 610323F3498FEA93EE8D7ECD /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 859D7659433CF3D1320F86CC /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 8B52615E2A7C463D00E9899E /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 8B5261602A7C463D00E9899E /* powersync_flutter_demoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = powersync_flutter_demoTests.swift; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - 9DCB9EDE28DF57E29440CF22 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - AF676D80A0CF80705DF388CF /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - C1A05183B57D5869377A17B4 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - C2FC729F34600C40853A030B /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - D0A6A6185A7A65698B8F4B1D /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 991B2149AEB0B3DAB09CB3BE /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AE0A38BE3137E8F7E92FEF51 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + D7EC9DA661EA44265DC94A0B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + DD914308A8C7B352FD10170F /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + EE686A5A317D10AC330E1BF9 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -95,7 +95,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 04EE2EEA1AF4432FCFE4D947 /* Pods_Runner.framework in Frameworks */, + 75E48BA0AEB945CF7281B8D7 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -103,7 +103,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 2F56F886B3B1884D3E437FD0 /* Pods_RunnerTests.framework in Frameworks */, + 9381A48772266D9C49309994 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -128,8 +128,8 @@ 33CEB47122A05771004F2AC0 /* Flutter */, 8B52615F2A7C463D00E9899E /* powersync_flutter_demoTests */, 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, - B6C445B3E9905835336FDF92 /* Pods */, + 85152821BF893F70AB178223 /* Pods */, + B7D43289954627B8B1B9B1F8 /* Frameworks */, ); sourceTree = ""; }; @@ -177,33 +177,33 @@ path = Runner; sourceTree = ""; }; - 8B52615F2A7C463D00E9899E /* powersync_flutter_demoTests */ = { + 85152821BF893F70AB178223 /* Pods */ = { isa = PBXGroup; children = ( - 8B5261602A7C463D00E9899E /* powersync_flutter_demoTests.swift */, + 4D425E3DE8C8153AB8C55A47 /* Pods-Runner.debug.xcconfig */, + D7EC9DA661EA44265DC94A0B /* Pods-Runner.release.xcconfig */, + EE686A5A317D10AC330E1BF9 /* Pods-Runner.profile.xcconfig */, + AE0A38BE3137E8F7E92FEF51 /* Pods-RunnerTests.debug.xcconfig */, + 10C22D07423926C20887DE48 /* Pods-RunnerTests.release.xcconfig */, + DD914308A8C7B352FD10170F /* Pods-RunnerTests.profile.xcconfig */, ); - path = powersync_flutter_demoTests; + name = Pods; + path = Pods; sourceTree = ""; }; - B6C445B3E9905835336FDF92 /* Pods */ = { + 8B52615F2A7C463D00E9899E /* powersync_flutter_demoTests */ = { isa = PBXGroup; children = ( - 1FB90A99EA939D06EE287C09 /* Pods-Runner.debug.xcconfig */, - AF676D80A0CF80705DF388CF /* Pods-Runner.release.xcconfig */, - C1A05183B57D5869377A17B4 /* Pods-Runner.profile.xcconfig */, - 9DCB9EDE28DF57E29440CF22 /* Pods-RunnerTests.debug.xcconfig */, - D0A6A6185A7A65698B8F4B1D /* Pods-RunnerTests.release.xcconfig */, - 859D7659433CF3D1320F86CC /* Pods-RunnerTests.profile.xcconfig */, + 8B5261602A7C463D00E9899E /* powersync_flutter_demoTests.swift */, ); - name = Pods; - path = Pods; + path = powersync_flutter_demoTests; sourceTree = ""; }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { + B7D43289954627B8B1B9B1F8 /* Frameworks */ = { isa = PBXGroup; children = ( - 386AF35B349F70B5D676F5EC /* Pods_Runner.framework */, - C2FC729F34600C40853A030B /* Pods_RunnerTests.framework */, + 610323F3498FEA93EE8D7ECD /* Pods_Runner.framework */, + 991B2149AEB0B3DAB09CB3BE /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -215,13 +215,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 9E5C59BA43BACEF39908FBDE /* [CP] Check Pods Manifest.lock */, + 179983970B20315AFC123D9D /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, - 44F34942EBFBB7F6E89ED4BA /* [CP] Embed Pods Frameworks */, + F30CD4FEA1BDB49C8B84FB01 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -237,7 +237,7 @@ isa = PBXNativeTarget; buildConfigurationList = 8B5261672A7C463D00E9899E /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 9BD4CD7B4DFE9A6CC5BE206C /* [CP] Check Pods Manifest.lock */, + 6C234BEFE02FE63D2DBBAA50 /* [CP] Check Pods Manifest.lock */, 8B52615A2A7C463D00E9899E /* Sources */, 8B52615B2A7C463D00E9899E /* Frameworks */, 8B52615C2A7C463D00E9899E /* Resources */, @@ -322,62 +322,67 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 3399D490228B24CF009A79C7 /* ShellScript */ = { + 179983970B20315AFC123D9D /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; }; - 33CC111E2044C6BF0003C045 /* ShellScript */ = { + 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - Flutter/ephemeral/FlutterInputs.xcfilelist, ); inputPaths = ( - Flutter/ephemeral/tripwire, ); outputFileListPaths = ( - Flutter/ephemeral/FlutterOutputs.xcfilelist, ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; }; - 44F34942EBFBB7F6E89ED4BA /* [CP] Embed Pods Frameworks */ = { + 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, ); - name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; - 9BD4CD7B4DFE9A6CC5BE206C /* [CP] Check Pods Manifest.lock */ = { + 6C234BEFE02FE63D2DBBAA50 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -399,26 +404,21 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 9E5C59BA43BACEF39908FBDE /* [CP] Check Pods Manifest.lock */ = { + F30CD4FEA1BDB49C8B84FB01 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -508,7 +508,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -587,7 +587,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -634,7 +634,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -699,7 +699,7 @@ }; 8B5261642A7C463D00E9899E /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9DCB9EDE28DF57E29440CF22 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = AE0A38BE3137E8F7E92FEF51 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -729,7 +729,7 @@ }; 8B5261652A7C463D00E9899E /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D0A6A6185A7A65698B8F4B1D /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 10C22D07423926C20887DE48 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -758,7 +758,7 @@ }; 8B5261662A7C463D00E9899E /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 859D7659433CF3D1320F86CC /* Pods-RunnerTests.profile.xcconfig */; + baseConfigurationReference = DD914308A8C7B352FD10170F /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; diff --git a/demos/supabase-todolist/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demos/supabase-todolist/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 992a778b..943aed19 100644 --- a/demos/supabase-todolist/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/demos/supabase-todolist/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -59,6 +59,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/demos/supabase-todolist/macos/Runner/AppDelegate.swift b/demos/supabase-todolist/macos/Runner/AppDelegate.swift index d53ef643..b3c17614 100644 --- a/demos/supabase-todolist/macos/Runner/AppDelegate.swift +++ b/demos/supabase-todolist/macos/Runner/AppDelegate.swift @@ -1,9 +1,13 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } } diff --git a/demos/supabase-todolist/macos/RunnerTests/RunnerTests.swift b/demos/supabase-todolist/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..61f3bd1f --- /dev/null +++ b/demos/supabase-todolist/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/demos/supabase-todolist/pubspec.lock b/demos/supabase-todolist/pubspec.lock index c88b85fe..24fad347 100644 --- a/demos/supabase-todolist/pubspec.lock +++ b/demos/supabase-todolist/pubspec.lock @@ -1,14 +1,30 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f + url: "https://pub.dev" + source: hosted + version: "85.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d" + url: "https://pub.dev" + source: hosted + version: "7.7.1" app_links: dependency: transitive description: name: app_links - sha256: f04c3ca96426baba784c736a201926bd4145524c36a1b38942a351b033305e21 + sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8" url: "https://pub.dev" source: hosted - version: "6.2.1" + version: "6.4.1" app_links_linux: dependency: transitive description: @@ -37,34 +53,34 @@ packages: dependency: transitive description: name: archive - sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" url: "https://pub.dev" source: hosted - version: "3.6.1" + version: "4.0.7" args: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.7.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" camera: dependency: "direct main" description: @@ -77,26 +93,26 @@ packages: dependency: transitive description: name: camera_android - sha256: "32f04948a284b71d938fe275616faf4957d07f9b3aab8021bfc8c418301a289e" + sha256: "4db8a27da163130d913ab4360297549ead1c7f9a6a88e71c44e5f4d10081a3d4" url: "https://pub.dev" source: hosted - version: "0.10.9+11" + version: "0.10.10+6" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: "7c28969a975a7eb2349bc2cb2dfe3ad218a33dba9968ecfb181ce08c87486655" + sha256: "951ef122d01ebba68b7a54bfe294e8b25585635a90465c311b2f875ae72c412f" url: "https://pub.dev" source: hosted - version: "0.9.17+3" + version: "0.9.21+2" camera_platform_interface: dependency: transitive description: name: camera_platform_interface - sha256: b3ede1f171532e0d83111fe0980b46d17f1aa9788a07a2fbed07366bbdbb9061 + sha256: "2f757024a48696ff4814a789b0bd90f5660c0fb25f393ab4564fb483327930e2" url: "https://pub.dev" source: hosted - version: "2.8.0" + version: "2.10.0" camera_web: dependency: transitive description: @@ -109,98 +125,106 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" checked_yaml: dependency: transitive description: name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.4" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" - cross_file: + version: "1.19.1" + convert: dependency: transitive description: - name: cross_file - sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "0.3.4+2" - crypto: + version: "3.1.2" + coverage: dependency: transitive description: - name: crypto - sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 + name: coverage + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" url: "https://pub.dev" source: hosted - version: "3.0.5" - fake_async: + version: "1.15.0" + cross_file: dependency: transitive description: - name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" url: "https://pub.dev" source: hosted - version: "1.3.1" - fetch_api: + version: "0.3.4+2" + crypto: dependency: transitive description: - name: fetch_api - sha256: "97f46c25b480aad74f7cc2ad7ccba2c5c6f08d008e68f95c1077286ce243d0e6" + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "2.2.0" - fetch_client: + version: "3.0.6" + fake_async: dependency: transitive description: - name: fetch_client - sha256: "9666ee14536778474072245ed5cba07db81ae8eb5de3b7bf4a2d1e2c49696092" + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.3.3" ffi: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -218,10 +242,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "9d98bd47ef9d34e803d438f17fd32b116d31009f534a6fa5ce3a1167f189a6de" + sha256: b0694b7fb1689b0e6cc193b3f1fcac6423c4f93c74fb20b806c6b6f196db0c31 url: "https://pub.dev" source: hosted - version: "2.0.21" + version: "2.0.30" flutter_test: dependency: "direct dev" description: flutter @@ -232,22 +256,38 @@ packages: description: flutter source: sdk version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" functions_client: dependency: transitive description: name: functions_client - sha256: e63f49cd3b41727f47b3bde284a11a4ac62839e0604f64077d4257487510e484 + sha256: "38e5049d4ca5b3482c606d8bfe82183aa24c9650ef1fa0582ab5957a947b937f" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.4" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" gotrue: dependency: transitive description: name: gotrue - sha256: "8703db795511f69194fe77125a0c838bbb6befc2f95717b6e40331784a8bdecb" + sha256: "4ed944bfa31cca12e6d224ed07557ccf1bb604959de3c7d5282cd11314e7655b" url: "https://pub.dev" source: hosted - version: "2.8.4" + version: "2.14.0" gtk: dependency: transitive description: @@ -260,34 +300,50 @@ packages: dependency: transitive description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.5.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" image: dependency: "direct main" description: name: image - sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + url: "https://pub.dev" + source: hosted + version: "4.5.4" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "1.0.5" js: dependency: transitive description: name: js - sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "0.7.2" json_annotation: dependency: transitive description: @@ -308,26 +364,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "11.0.1" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -340,18 +396,18 @@ packages: dependency: "direct main" description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -364,18 +420,18 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mime: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "2.0.0" mutex: dependency: transitive description: @@ -384,38 +440,54 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" path: dependency: "direct main" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_provider: dependency: "direct main" description: name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" + sha256: "993381400e94d18469750e5b9dcb8206f15bc09f9da86b9e44a9b0092a0066db" url: "https://pub.dev" source: hosted - version: "2.2.10" + version: "2.2.18" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.2" path_provider_linux: dependency: transitive description: @@ -444,18 +516,18 @@ packages: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "7.0.1" platform: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -464,59 +536,82 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" postgrest: dependency: transitive description: name: postgrest - sha256: c4197238601c7c3103b03a4bb77f2050b17d0064bf8b968309421abdebbb7f0e + sha256: "10b81a23b1c829ccadf68c626b4d66666453a1474d24c563f313f5ca7851d575" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.4.2" powersync: dependency: "direct main" description: path: "../../packages/powersync" relative: true source: path - version: "1.9.2" + version: "1.15.2" powersync_attachments_helper: - dependency: "direct main" + dependency: "direct overridden" description: path: "../../packages/powersync_attachments_helper" relative: true source: path - version: "0.6.15+1" + version: "0.6.19" + powersync_core: + dependency: "direct main" + description: + path: "../../packages/powersync_core" + relative: true + source: path + version: "1.5.2" powersync_flutter_libs: dependency: "direct overridden" description: path: "../../packages/powersync_flutter_libs" relative: true source: path - version: "0.4.2" + version: "0.4.11" pub_semver: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" realtime_client: dependency: transitive description: name: realtime_client - sha256: d897a65ee3b1b5ddc1cf606f0b83792262d38fd5679c2df7e38da29c977513da + sha256: "025b7e690e8dcf27844f37d140cca47da5ab31d6fe8d78347fb16763f0a4beb6" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.5.2" retry: dependency: transitive description: @@ -537,26 +632,26 @@ packages: dependency: transitive description: name: shared_preferences - sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: a7e8467e9181cef109f601e3f65765685786c1a738a83d7fbbde377589c0d974 + sha256: a2608114b1ffdcbc9c120eb71a0e207c71da56202852d4aab8a5e30a82269e74 url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.12" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.4" shared_preferences_linux: dependency: transitive description: @@ -577,10 +672,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" shared_preferences_windows: dependency: transitive description: @@ -589,19 +684,67 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" sprintf: dependency: transitive description: @@ -614,114 +757,130 @@ packages: dependency: transitive description: name: sqlite3 - sha256: bb174b3ec2527f9c5f680f73a89af8149dd99782fbb56ea88ad0807c5638f2ed + sha256: f393d92c71bdcc118d6203d07c991b9be0f84b1a6f89dd4f7eed348131329924 url: "https://pub.dev" source: hosted - version: "2.4.7" + version: "2.9.0" sqlite3_flutter_libs: dependency: transitive description: name: sqlite3_flutter_libs - sha256: "62bbb4073edbcdf53f40c80775f33eea01d301b7b81417e5b3fb7395416258c1" + sha256: "2b03273e71867a8a4d030861fc21706200debe5c5858a4b9e58f4a1c129586a4" url: "https://pub.dev" source: hosted - version: "0.5.24" + version: "0.5.39" sqlite3_web: dependency: transitive description: name: sqlite3_web - sha256: f22d1dda7a40be0867984f55cdf5c2d599e5f05d3be4a642d78f38b38983f554 + sha256: "0f6ebcb4992d1892ac5c8b5ecd22a458ab9c5eb6428b11ae5ecb5d63545844da" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.3.2" sqlite_async: dependency: "direct main" description: name: sqlite_async - sha256: d66fb6e6d07c1a834743326c033029f75becbb1fad6823d709f921872abc3d5b + sha256: "6116bfc6aef6ce77730b478385ba4a58873df45721f6a9bc6ffabf39b6576e36" url: "https://pub.dev" source: hosted - version: "0.11.0" + version: "0.12.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" storage_client: dependency: transitive description: name: storage_client - sha256: "28c147c805304dbc2b762becd1fc26ee0cb621ace3732b9ae61ef979aab8b367" + sha256: "1c61b19ed9e78f37fdd1ca8b729ab8484e6c8fe82e15c87e070b861951183657" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.4.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" supabase: dependency: transitive description: name: supabase - sha256: "4ed1cf3298f39865c05b2d8557f92eb131a9b9af70e32e218672a0afce01a6bc" + sha256: da2afea0f06b06fd0ebb23b916af05df2ef6d56dccae5c0112a2171e668bf9b2 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.9.0" supabase_flutter: dependency: "direct main" description: name: supabase_flutter - sha256: ff6ba3048fd47d831fdc0027d3efb99346d99b95becfcb406562454bd9b229c5 + sha256: f7eefb065f00f947a8f2d0fbb4be3e687ed7165e80c798bc8c4f0d4295855c51 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.10.0" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" + test: + dependency: "direct dev" + description: + name: test + sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" + url: "https://pub.dev" + source: hosted + version: "1.26.2" test_api: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.6" + test_core: + dependency: transitive + description: + name: test_core + sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" + url: "https://pub.dev" + source: hosted + version: "0.6.11" typed_data: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" universal_io: dependency: "direct main" description: @@ -734,42 +893,42 @@ packages: dependency: transitive description: name: url_launcher - sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.2" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: f0c73347dfcfa5b3db8bc06e1502668265d39c08f310c29bff4e28eea9699f79 + sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7" url: "https://pub.dev" source: hosted - version: "6.3.9" + version: "6.3.18" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e + sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7 url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.4" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.3" url_launcher_platform_interface: dependency: transitive description: @@ -782,98 +941,114 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.1" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.4" uuid: dependency: transitive description: name: uuid - sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "4.4.2" + version: "4.5.1" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c" + url: "https://pub.dev" + source: hosted + version: "1.1.3" web: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web_socket: dependency: transitive description: name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.6.1" yaml: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" yet_another_json_isolate: dependency: transitive description: name: yet_another_json_isolate - sha256: "47ed3900e6b0e4dfe378811a4402e85b7fc126a7daa94f840fef65ea9c8e46f4" + sha256: fe45897501fa156ccefbfb9359c9462ce5dec092f05e8a56109db30be864f01e url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + dart: ">=3.8.0 <4.0.0" + flutter: ">=3.29.0" diff --git a/demos/supabase-todolist/pubspec.yaml b/demos/supabase-todolist/pubspec.yaml index c13903d8..2175e2b4 100644 --- a/demos/supabase-todolist/pubspec.yaml +++ b/demos/supabase-todolist/pubspec.yaml @@ -10,8 +10,8 @@ environment: dependencies: flutter: sdk: flutter - powersync_attachments_helper: ^0.6.15+2 - powersync: ^1.9.3 + powersync: ^1.16.1 + powersync_core: ^1.6.1 path_provider: ^2.1.1 supabase_flutter: ^2.0.1 path: ^1.8.3 @@ -19,13 +19,18 @@ dependencies: camera: ^0.10.5+7 image: ^4.1.3 universal_io: ^2.2.2 - sqlite_async: ^0.11.0 + sqlite_async: ^0.12.0 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^3.0.1 + test: ^1.25.15 flutter: uses-material-design: true + config: + # PowerSync supports SPM, but we want to disable it in this demo so that we can keep testing + # the CocoaPods parts of powersync_flutter_libs. + enable-swift-package-manager: false diff --git a/demos/supabase-trello/.env.template b/demos/supabase-trello/.env.template new file mode 100644 index 00000000..073dd3bb --- /dev/null +++ b/demos/supabase-trello/.env.template @@ -0,0 +1,4 @@ +# Update these with your own values +SUPABASE_URL=https://aaaaaaaaaaaa.supabase.co +SUPABASE_ANON_KEY=my_anon_key +POWERSYNC_URL=https://aaaaaaaaaaaa.powersync.journeyapps.com diff --git a/demos/supabase-trello/.gitignore b/demos/supabase-trello/.gitignore new file mode 100644 index 00000000..7f4b32b9 --- /dev/null +++ b/demos/supabase-trello/.gitignore @@ -0,0 +1,47 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +.env + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/demos/supabase-trello/.metadata b/demos/supabase-trello/.metadata new file mode 100644 index 00000000..2e239dba --- /dev/null +++ b/demos/supabase-trello/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: android + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: ios + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: linux + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: macos + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: web + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: windows + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/demos/supabase-trello/LICENSE b/demos/supabase-trello/LICENSE new file mode 100644 index 00000000..0e259d42 --- /dev/null +++ b/demos/supabase-trello/LICENSE @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/demos/supabase-trello/README.md b/demos/supabase-trello/README.md new file mode 100644 index 00000000..a267c88e --- /dev/null +++ b/demos/supabase-trello/README.md @@ -0,0 +1,342 @@ +# Trello Clone Using PowerSync + Flutter + +## Introduction + +Trello clone app built with [Flutter](https://flutter.dev/), [PowerSync](https://powersync.co/) and [Supabase](https://supabase.io/) + +drawing + +# Running the app + +Ensure you have [melos](https://melos.invertase.dev/~melos-latest/getting-started) installed. + +1. `cd demos/supabase-trello` +2. `melos prepare` +3. `cp .env.template .env` +4. Insert your Supabase and PowerSync project credentials into `.env` (See instructions below) +5. `flutter run` + +# Getting Started + +First check out [the integration guide](https://docs.powersync.co/integration-guides/supabase-+-powersync) for [PowerSync](https://powersync.co/) and [Supabase](https://supabase.io/). + +Before you proceed, we assume that you have already signed up for free accounts with both Supabase and PowerSync. If you haven't signed up for a **PowerSync account** yet, [click here](https://accounts.journeyapps.com/portal/free-trial?powersync=true) (and if you haven't signed up for **Supabase** yet, [click here](https://supabase.com/dashboard/sign-up)). We also assume that you already have Flutter set up. + +Next up we will follow these steps: + +1. Configure Supabase: + - Create the database schema + - Create the Postgres publication +2. Configure PowerSync: + - Create connection to Supabase + - Configure global Sync Rules +4. Configure the Trello clone app +5. Run the app and test +6. Configure improved Sync Rules +7. Run the app and test + +## Configure Supabase + +- Create a new Supabase project (or use an existing project if you prefer) and follow the below steps. +- For ease of use of this Flutter app, you can **disable email confirmation** in your Supabase Auth settings. In your Supabase project, go to _"Authentication"_ --> _"Providers"_ -> _"Email"_ and then disable _"Confirm email"_. If you keep email confirmation enabled, the Supabase user confirmation email will reference the default Supabase Site URL of http://localhost:3000 — you can ignore this. + +### Create the Database Schema +After creating the Supabase project, we still need to create the tables in the database. For this application we need the following tables: + +* `activity` +* `attachment` +* `board` +* `card` +* `checklist` +* `comment` +* `listboard` +* `member` +* `models` +* `trellouser` +* `workspace` +* `board_label` +* `card_label` + +_(We give a brief overview of the app domain model later in this README.)_ + +Do the following: +- Open the `tables.sql` file, and copy the contents. +- Paste this into the *Supabase SQL Editor* +- Run the SQL statements in the Supabase SQL Editor. (If you get a warning about a "potentially destructive operation", that's a false positive that you can safely ignore.) + +### Create the Postgres Publication + +PowerSync uses the Postgres [Write Ahead Log (WAL)](https://www.postgresql.org/docs/current/wal-intro.html) to replicate data changes in order to keep PowerSync SDK clients up to date. To enable this we need to create a `publication` in Supabase. + +Run the below SQL statement in your Supabase SQL Editor: +```sql +create publication powersync for table activity, attachment, board, card, checklist, comment, listboard, member, trellouser, workspace, board_label, card_label; +``` + +## Configuring PowerSync + +We need to connect PowerSync to the Supabase Postgres database: + +- In the [PowerSync dashboard](https://powersync.journeyapps.com/) project tree, click on _"Create new instance"_. + +- Give your instance a name, such as _"Trello Clone"_. + +- In the _"Edit Instance"_ dialog, navigate to the _"Credentials"_ tab and enable _"Use Supabase Auth"_. + +- Under the _"Connections"_ tab, click on the + icon. + +- On the subsequent screen, we'll configure the connection to Supabase. This is simplest using your Supabase database URI. In your Supabase dashboard, navigate to _"Project Settings"_ -> _"Database"_. Then, under the _"Connection String"_ section, switch to URI and copy the value. + +- Paste the copied value into the _"URI"_ field in PowerSync. + +- Enter the Password for the `postgres` user in your Supabase database. (Supabase also [refers to this password](https://supabase.com/docs/guides/database/managing-passwords) as the database password or project password) + +- Click _"Test Connection"_ and fix any errors. + +- Click "Save" + +PowerSync deploys and configures an isolated cloud environment for you, which will take a few minutes to complete. + + +## Configuring Sync Rules - 1 + +PowerSync [Sync Rules](https://docs.powersync.co/usage/sync-rules) allow developers to control which data gets synced to which user devices using a SQL-like syntax in a YAML file. For this Trello clone demo app, we're first going to use naive global sync rules, and then present improved rules that take the domain permissions into account. + + +### Global Sync Rules to Get Things Working + +We can be naive about it, and start by using a global bucket definition that at least specifies in some way which users can get data. + +- Copy the contents of `sync-rules-0.yaml` to `sync-rules.yaml` under your PowerSync project instance. +- In the top right of the `sync-rules.yaml` editor, click _"Deploy sync rules"_. +- Confirm in the dialog and wait a couple of minutes for the deployment to complete. + +When you now run the app (after completing the next step to configure and run the app), it will actually show and retain data. The app code itself applies some basic filtering to only show data that belongs to the current user, or according to the visibility and membership settings of the various workspaces and boards. + + +## Configuring Flutter App + +We need to configure the app to use the correct PowerSync and Supabase projects. + +- Copy the `trelloappclone_flutter/.env.template` file to `trelloappclone_flutter/.env`. +- Replace the values for `SUPABASE_URL` and `SUPABASE_ANON_KEY` (You can find these under _"Project Settings"_ -> _"API"_ in your Supabase dashboard — under the _"URL"_ section, and anon key under _"Project API keys"_.) +- For the value of `POWERSYNC_URL`, follow these steps: + 1. In the project tree on the PowerSync dashboard, right-click on the instance you created earlier. + 2. Click _"Edit instance"_. + 3. Click on _"Instance URL"_ to copy the value. + + +## Build & Run the Flutter App + +- Run ``` flutter pub get ``` to install the necessary packages (in the root directory of the project.) +- Invoke the ``` flutter run ``` command, and select either an Android device/emulator or iOS device/simulator as destination (_Note: PowerSync does not support Flutter web apps yet._) + + +## Configuring Sync Rules - 2 + +### Using Sync Rules to Enforce Permissions +We have syncing working, but the sync rules are not enforcing the access rules from the domain in any way. + +It is better that we do not sync data to the client that the logged-in user is not allowed to see. We can use PowerSync sync rules to enforce permissions, so that users can only see and edit data that they are allowed to see and edit. + +First, we need to understand the permissions from the app domain model: + +- A **workspace** is created by a user — this user can always see and edit the workspace. +- A **workspace** has a specific *visibility*: private (only the owner can see it), workspace (only owner and members can see it), or public (anyone can see it). +- A **workspace** has a list of *members* (users) that can see and edit the workspace, if the workspace is not private. +- A **board** is created by a user — this user can always see and edit the board as long as the user can still access that workspace +- A **board** has a specific *visibility*: private (only the owner can see it), workspace (only owner and members belonging to the parent workspace can see it) +- A user can see (and edit) any of the **cards** and **lists** belonging to a **board** that they have access to. +- A user can see (and edit) any of the **checklists**, **comments**, and **attachments** belonging to a **card** that they have access to. + +Also have a look at `trelloappclone_flutter/lib/utils/service.dart` for the access patterns used by the app code. + +Let's explore how we can use PowerSync Sync Rules to enforce these permissions and access patterns. + +First we want to sync the relevant `trellouser` records synced to the local database. To enable lookups of users for adding as members to workspaces, we currently sync all user records. For a production app, we would ideally work via an API to invite members, and not worry about direct data lookups on the app side. + +```yaml +bucket_definitions: + user_info: + # this allows syncing of all trellouser records so we can lookup users when adding members + data: + - SELECT * FROM trellouser +``` + +Then we want to look up all the workspaces (a) owned by this user, (b) where this user is a member, or (c) which are public. + +```yaml + by_workspace: + # the entities are filtered by workspaceId, thus linked to the workspaces (a) owned by this user, (b) where this user is a member, or (c) which are public + # Note: the quotes for "workspaceId" and "userId" is important, since otherwise Postgres does not deal well with non-lowercase identifiers + parameters: + - SELECT id as workspace_id FROM workspace WHERE + workspace."userId" = token_parameters.user_id + - SELECT "workspaceId" as workspace_id FROM member WHERE + member."userId" = token_parameters.user_id + - SELECT id as workspace_id FROM workspace WHERE + visibility = "Public" + data: + - SELECT * FROM workspace WHERE workspace.id = bucket.workspace_id + - SELECT * FROM board WHERE board."workspaceId" = bucket.workspace_id + - SELECT * FROM member WHERE member."workspaceId" = bucket.workspace_id + - SELECT * FROM listboard WHERE listboard."workspaceId" = bucket.workspace_id + - SELECT * FROM card WHERE card."workspaceId" = bucket.workspace_id + - SELECT * FROM checklist WHERE checklist."workspaceId" = bucket.workspace_id + - SELECT * FROM activity WHERE activity."workspaceId" = bucket.workspace_id + - SELECT * FROM comment WHERE comment."workspaceId" = bucket.workspace_id + - SELECT * FROM attachment WHERE attachment."workspaceId" = bucket.workspace_id + - SELECT * FROM board_label WHERE board_label."workspaceId" = bucket.workspace_id + - SELECT * FROM card_label WHERE card_label."workspaceId" = bucket.workspace_id +``` + +**To Configure the Improved Sync Rules, Follow These Steps:** + +- Copy the contents of `sync-rules-1.yaml`. +- Paste this to `sync-rules.yaml` under your PowerSync project instance +- Click _"Deploy sync rules"_. +- Confirm in the dialog and wait a couple of minutes for the deployment to complete. + +Now you can run the app again, and it should now only sync the subset of data that a logged in user actually has access to. + +### Importing / Generating Data + +When you run the app, after logging in, you will start without any workspaces or boards. It is possible to generate a workspace with sample boards and cards in order to make it easier to have enough data to experiment with, without having to manually create every item. + +- Sign up and log in to the app. +- In the home view, tap on the "+" floating button in the lower right corner. +- Tap on _"Sample Workspace"_ and give it a name — this will create a new workspace, with multiple boards with lists, and a random number of cards with checklists, comments and activities for each list. + +drawing + +### Structure + +* The data models are in `lib/models`) +* A PowerSync client that makes use of the PowerSync Flutter SDK and adds a few convenience methods for the app use cases (`lib/protocol/powersync.dart`) +* A `DataClient` API that can be used from the app code, and provides the higher level data API. (`lib/protocol/data_client.dart`) + +Two important files to point out as well are: + +* `lib/schema.dart` defines the SQLite schema to use for the local synced datastore, and this maps to the model classes. +* `lib/app_config.dart` contains the tokens and URLs needed to connect to PowerSync and Supabase. + + +### Listening to Updated Data + +The PowerSync SDK makes use of `watch` queries to listen for changes to synced data. When the SDK syncs updated data from the server, and it matches the query, it will send an event out, allowing e.g. an app view (via `StreamBuilder`) or some other state management class to respond. + +As an example look at `trelloappclone_flutter/lib/utils/service.dart`: + +```dart + Stream> getWorkspacesStream() { + return dataClient.workspace.watchWorkspacesByUser(userId: trello.user.id).map((workspaces) { + trello.setWorkspaces(workspaces); + return workspaces; + }); + } +``` + +This in turn makes use of the `_WorkspaceRepository` class: +```dart + Stream> watchWorkspacesByUser({required String userId}) { + //First we get the workspaces + return client.getDBExecutor().watch(''' + SELECT * FROM workspace WHERE userId = ? + ''', parameters: [userId]).asyncMap((event) async { + List workspaces = event.map((row) => Workspace.fromRow(row)).toList(); + + //Then we get the members for each workspace + for (Workspace workspace in workspaces) { + List members = await client.member.getMembersByWorkspace(workspaceId: workspace.id); + workspace.members = members; + } + return workspaces; + }); + } +``` + +Now we can simply make use of a `StreamBuilder` to have a view component that updates whenever a matching workspace is updated: + +```dart + StreamBuilder( + stream: getWorkspacesStream(), + builder: + (BuildContext context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + List children = snapshot.data as List; + + if (children.isNotEmpty) { + return SingleChildScrollView( + child: + Column(children: buildWorkspacesAndBoards(children))); + } + } else { + return Container(); + } + }) +``` + +### Doing a Transaction + +It is possible to ensure that a number of related entities are updated in an atomic database transaction. As an example of this, look at `DataClient.archiveCardsInList`: + +```dart + /// Archive cards in and return how many were archived + /// This happens in a transaction + Future archiveCardsInList(Listboard list) async { + if (list.cards == null || list.cards!.isEmpty) { + return 0; + } + + //start a write transaction + return client.getDBExecutor().writeTransaction((sqlContext) async { + List cards = list.cards!; + int numCards = cards.length; + + //we set each of the cards in the list to archived = true + sqlContext.executeBatch(''' + UPDATE card + SET archived = 1 + WHERE id = ? + ''' + , cards.map((card) => [card.id]).toList()); + + //touch listboard to trigger update via stream listeners on Listboard + sqlContext.execute(''' + UPDATE listboard + SET archived = 0 + WHERE id = ? + ''', [list.id]); + + list.cards = []; + return numCards; + //end of transaction + }, debugContext: 'archiveCardsInList'); + } +``` + +The above code is invoked if you choose to archive all the cards in a list from the list popup menu. + +### Changes from Original Trello Clone App + +The app code was forked from the [Serverpod + Flutter Tutorial](https://github.com/Mobterest/serverpod_flutter_tutorial) code. It was changed in the following ways to facilitate the PowerSync integration: + +- Updated data model so that all `id` fields are Strings, and use UUIDs (it was auto-increment integer fields in the original app) +- Updated data model so that all entities refer to the `workspaceId` of workspace in which it was created (this facilitates the Sync Rules) + +## Next + +Below is a list of things that can be implemented to enhance the functionality and experience of this app: + +* Update Workspace + Board edit views to use actual data and update the entity +* Get Comments & Checklists working properly +* Enhance state management - e.g. let `TrelloProvider` listen to streams, and notify changes, to simplify views +* Get the attachments to actually work (using Supabase files upload/download) + +## Feedback + +- Feel free to send feedback. Feature requests are always welcome. If there's anything you'd like to chat about, please [join our Discord](https://discord.gg/powersync). + +## Acknowledgements + +This tutorial is based on the [Serverpod + Flutter Tutorial](https://github.com/Mobterest/serverpod_flutter_tutorial) by [Mobterest](https://www.instagram.com/mobterest/) diff --git a/demos/supabase-trello/analysis_options.yaml b/demos/supabase-trello/analysis_options.yaml new file mode 100644 index 00000000..61b6c4de --- /dev/null +++ b/demos/supabase-trello/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/demos/supabase-trello/android/.gitignore b/demos/supabase-trello/android/.gitignore new file mode 100644 index 00000000..6f568019 --- /dev/null +++ b/demos/supabase-trello/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/demos/supabase-trello/android/app/build.gradle b/demos/supabase-trello/android/app/build.gradle new file mode 100644 index 00000000..21dc0f37 --- /dev/null +++ b/demos/supabase-trello/android/app/build.gradle @@ -0,0 +1,65 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +android { + namespace "com.example.trelloappclone_flutter" + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.trelloappclone_flutter" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion 23 + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} diff --git a/demos/supabase-trello/android/app/src/debug/AndroidManifest.xml b/demos/supabase-trello/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/demos/supabase-trello/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/demos/supabase-trello/android/app/src/main/AndroidManifest.xml b/demos/supabase-trello/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..e1dc4071 --- /dev/null +++ b/demos/supabase-trello/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/demos/supabase-trello/android/app/src/main/kotlin/com/example/trelloappclone_flutter/MainActivity.kt b/demos/supabase-trello/android/app/src/main/kotlin/com/example/trelloappclone_flutter/MainActivity.kt new file mode 100644 index 00000000..875d532e --- /dev/null +++ b/demos/supabase-trello/android/app/src/main/kotlin/com/example/trelloappclone_flutter/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.trelloappclone_flutter + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/demos/supabase-trello/android/app/src/main/res/drawable-v21/launch_background.xml b/demos/supabase-trello/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/demos/supabase-trello/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/demos/supabase-trello/android/app/src/main/res/drawable/launch_background.xml b/demos/supabase-trello/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/demos/supabase-trello/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/demos/supabase-trello/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/supabase-trello/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..db77bb4b Binary files /dev/null and b/demos/supabase-trello/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/demos/supabase-trello/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/supabase-trello/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..17987b79 Binary files /dev/null and b/demos/supabase-trello/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/demos/supabase-trello/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/supabase-trello/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..09d43914 Binary files /dev/null and b/demos/supabase-trello/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/demos/supabase-trello/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/supabase-trello/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..d5f1c8d3 Binary files /dev/null and b/demos/supabase-trello/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/demos/supabase-trello/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/supabase-trello/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..4d6372ee Binary files /dev/null and b/demos/supabase-trello/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/demos/supabase-trello/android/app/src/main/res/values-night/styles.xml b/demos/supabase-trello/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..06952be7 --- /dev/null +++ b/demos/supabase-trello/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/demos/supabase-trello/android/app/src/main/res/values/styles.xml b/demos/supabase-trello/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..cb1ef880 --- /dev/null +++ b/demos/supabase-trello/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/demos/supabase-trello/android/app/src/profile/AndroidManifest.xml b/demos/supabase-trello/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/demos/supabase-trello/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/demos/supabase-trello/android/build.gradle b/demos/supabase-trello/android/build.gradle new file mode 100644 index 00000000..bc157bd1 --- /dev/null +++ b/demos/supabase-trello/android/build.gradle @@ -0,0 +1,18 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/demos/supabase-trello/android/gradle.properties b/demos/supabase-trello/android/gradle.properties new file mode 100644 index 00000000..94adc3a3 --- /dev/null +++ b/demos/supabase-trello/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/demos/supabase-trello/android/gradle/wrapper/gradle-wrapper.properties b/demos/supabase-trello/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..348c409e --- /dev/null +++ b/demos/supabase-trello/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip diff --git a/demos/supabase-trello/android/settings.gradle b/demos/supabase-trello/android/settings.gradle new file mode 100644 index 00000000..e9862544 --- /dev/null +++ b/demos/supabase-trello/android/settings.gradle @@ -0,0 +1,25 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.7.0" apply false + id "org.jetbrains.kotlin.android" version "1.7.0" apply false +} + +include ":app" diff --git a/demos/supabase-trello/assets/landing.jpg b/demos/supabase-trello/assets/landing.jpg new file mode 100644 index 00000000..7614ae9f Binary files /dev/null and b/demos/supabase-trello/assets/landing.jpg differ diff --git a/demos/supabase-trello/assets/trello-logo.png b/demos/supabase-trello/assets/trello-logo.png new file mode 100644 index 00000000..a8068fd6 Binary files /dev/null and b/demos/supabase-trello/assets/trello-logo.png differ diff --git a/demos/supabase-trello/devtools_options.yaml b/demos/supabase-trello/devtools_options.yaml new file mode 100644 index 00000000..fa0b357c --- /dev/null +++ b/demos/supabase-trello/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/demos/supabase-trello/ios/.gitignore b/demos/supabase-trello/ios/.gitignore new file mode 100644 index 00000000..7a7f9873 --- /dev/null +++ b/demos/supabase-trello/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/demos/supabase-trello/ios/Flutter/AppFrameworkInfo.plist b/demos/supabase-trello/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 00000000..7c569640 --- /dev/null +++ b/demos/supabase-trello/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/demos/supabase-trello/ios/Flutter/Debug.xcconfig b/demos/supabase-trello/ios/Flutter/Debug.xcconfig new file mode 100644 index 00000000..ec97fc6f --- /dev/null +++ b/demos/supabase-trello/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/demos/supabase-trello/ios/Flutter/Release.xcconfig b/demos/supabase-trello/ios/Flutter/Release.xcconfig new file mode 100644 index 00000000..c4855bfe --- /dev/null +++ b/demos/supabase-trello/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/demos/supabase-trello/ios/Podfile b/demos/supabase-trello/ios/Podfile new file mode 100644 index 00000000..a8c080ff --- /dev/null +++ b/demos/supabase-trello/ios/Podfile @@ -0,0 +1,47 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' + end + end +end diff --git a/demos/supabase-trello/ios/Podfile.lock b/demos/supabase-trello/ios/Podfile.lock new file mode 100644 index 00000000..c47be6d3 --- /dev/null +++ b/demos/supabase-trello/ios/Podfile.lock @@ -0,0 +1,139 @@ +PODS: + - app_links (0.0.2): + - Flutter + - DKImagePickerController/Core (4.3.9): + - DKImagePickerController/ImageDataManager + - DKImagePickerController/Resource + - DKImagePickerController/ImageDataManager (4.3.9) + - DKImagePickerController/PhotoGallery (4.3.9): + - DKImagePickerController/Core + - DKPhotoGallery + - DKImagePickerController/Resource (4.3.9) + - DKPhotoGallery (0.0.19): + - DKPhotoGallery/Core (= 0.0.19) + - DKPhotoGallery/Model (= 0.0.19) + - DKPhotoGallery/Preview (= 0.0.19) + - DKPhotoGallery/Resource (= 0.0.19) + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Core (0.0.19): + - DKPhotoGallery/Model + - DKPhotoGallery/Preview + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Model (0.0.19): + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Preview (0.0.19): + - DKPhotoGallery/Model + - DKPhotoGallery/Resource + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Resource (0.0.19): + - SDWebImage + - SwiftyGif + - file_picker (0.0.1): + - DKImagePickerController/PhotoGallery + - Flutter + - Flutter (1.0.0) + - image_picker_ios (0.0.1): + - Flutter + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - powersync-sqlite-core (0.4.5) + - powersync_flutter_libs (0.0.1): + - Flutter + - powersync-sqlite-core (~> 0.4.5) + - SDWebImage (5.21.2): + - SDWebImage/Core (= 5.21.2) + - SDWebImage/Core (5.21.2) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (3.49.2): + - sqlite3/common (= 3.49.2) + - sqlite3/common (3.49.2) + - sqlite3/dbstatvtab (3.49.2): + - sqlite3/common + - sqlite3/fts5 (3.49.2): + - sqlite3/common + - sqlite3/math (3.49.2): + - sqlite3/common + - sqlite3/perf-threadsafe (3.49.2): + - sqlite3/common + - sqlite3/rtree (3.49.2): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (~> 3.49.1) + - sqlite3/dbstatvtab + - sqlite3/fts5 + - sqlite3/math + - sqlite3/perf-threadsafe + - sqlite3/rtree + - SwiftyGif (5.4.5) + - url_launcher_ios (0.0.1): + - Flutter + +DEPENDENCIES: + - app_links (from `.symlinks/plugins/app_links/ios`) + - file_picker (from `.symlinks/plugins/file_picker/ios`) + - Flutter (from `Flutter`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - powersync_flutter_libs (from `.symlinks/plugins/powersync_flutter_libs/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + +SPEC REPOS: + trunk: + - DKImagePickerController + - DKPhotoGallery + - powersync-sqlite-core + - SDWebImage + - sqlite3 + - SwiftyGif + +EXTERNAL SOURCES: + app_links: + :path: ".symlinks/plugins/app_links/ios" + file_picker: + :path: ".symlinks/plugins/file_picker/ios" + Flutter: + :path: Flutter + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + powersync_flutter_libs: + :path: ".symlinks/plugins/powersync_flutter_libs/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + sqlite3_flutter_libs: + :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + +SPEC CHECKSUMS: + app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7 + DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c + DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 + file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + powersync-sqlite-core: 6f32860379009d2a37cadc9e9427a431bdbd83c8 + powersync_flutter_libs: 7684a62208907328906eb932f1fc8b3d8879974e + SDWebImage: 9f177d83116802728e122410fb25ad88f5c7608a + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 + sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 + SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + +PODFILE CHECKSUM: 681bf989b1752c26661df140f63f5aad6922ddbb + +COCOAPODS: 1.16.2 diff --git a/demos/supabase-trello/ios/Runner.xcodeproj/project.pbxproj b/demos/supabase-trello/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..643e5165 --- /dev/null +++ b/demos/supabase-trello/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,723 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 580DE0ECE5B255011C07A13C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E2B5432F50F552DF6C1D0FA1 /* Pods_Runner.framework */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 9F5E6CDBDFD984AD1CE17EBF /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C37744B39F4E3B36A0BBB0CE /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0FF24B675B56F022C3289F08 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 36C14A9DF8CBED78B8383618 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 976B259CF18C36EB3B6F4F0F /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A2E15785CC927C31AEF6BC6A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + B0E8CE81B52F70CDF2A4F80B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + C37744B39F4E3B36A0BBB0CE /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DB8B11F7834AB262F47996E5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + E2B5432F50F552DF6C1D0FA1 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 580DE0ECE5B255011C07A13C /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A9A2ECF3A7DDE6AA422CD674 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9F5E6CDBDFD984AD1CE17EBF /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + A7F3D089A45655143E0E6848 /* Pods */, + B312AFB09377AB967113F8FE /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + A7F3D089A45655143E0E6848 /* Pods */ = { + isa = PBXGroup; + children = ( + DB8B11F7834AB262F47996E5 /* Pods-Runner.debug.xcconfig */, + 36C14A9DF8CBED78B8383618 /* Pods-Runner.release.xcconfig */, + 0FF24B675B56F022C3289F08 /* Pods-Runner.profile.xcconfig */, + A2E15785CC927C31AEF6BC6A /* Pods-RunnerTests.debug.xcconfig */, + B0E8CE81B52F70CDF2A4F80B /* Pods-RunnerTests.release.xcconfig */, + 976B259CF18C36EB3B6F4F0F /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + B312AFB09377AB967113F8FE /* Frameworks */ = { + isa = PBXGroup; + children = ( + E2B5432F50F552DF6C1D0FA1 /* Pods_Runner.framework */, + C37744B39F4E3B36A0BBB0CE /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 579312ED36242754EF3992BA /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + A9A2ECF3A7DDE6AA422CD674 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 5582B600ABA2BF2B04110F91 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + B27A73ED59948A115A9CD70F /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 5582B600ABA2BF2B04110F91 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 579312ED36242754EF3992BA /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + B27A73ED59948A115A9CD70F /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = K2CV557XCP; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.journeyapps.trelloappcloneFlutter; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A2E15785CC927C31AEF6BC6A /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.trelloappcloneFlutter.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B0E8CE81B52F70CDF2A4F80B /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.trelloappcloneFlutter.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 976B259CF18C36EB3B6F4F0F /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.trelloappcloneFlutter.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = K2CV557XCP; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.journeyapps.trelloappcloneFlutter; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = K2CV557XCP; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.journeyapps.trelloappcloneFlutter; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/demos/supabase-trello/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/demos/supabase-trello/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/demos/supabase-trello/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/demos/supabase-trello/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demos/supabase-trello/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/demos/supabase-trello/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/demos/supabase-trello/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/demos/supabase-trello/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/demos/supabase-trello/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/demos/supabase-trello/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demos/supabase-trello/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..8e3ca5df --- /dev/null +++ b/demos/supabase-trello/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/supabase-trello/ios/Runner.xcworkspace/contents.xcworkspacedata b/demos/supabase-trello/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/demos/supabase-trello/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/demos/supabase-trello/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demos/supabase-trello/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/demos/supabase-trello/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/demos/supabase-trello/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/demos/supabase-trello/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/demos/supabase-trello/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/demos/supabase-trello/ios/Runner/AppDelegate.swift b/demos/supabase-trello/ios/Runner/AppDelegate.swift new file mode 100644 index 00000000..b6363034 --- /dev/null +++ b/demos/supabase-trello/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d36b1fab --- /dev/null +++ b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 00000000..dc9ada47 Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 00000000..7353c41e Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 00000000..797d452e Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 00000000..6ed2d933 Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 00000000..4cd7b009 Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 00000000..fe730945 Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 00000000..321773cd Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 00000000..797d452e Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 00000000..502f463a Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 00000000..0ec30343 Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 00000000..0ec30343 Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 00000000..e9f5fea2 Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 00000000..84ac32ae Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 00000000..8953cba0 Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 00000000..0467bf12 Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 00000000..0bedcf2f --- /dev/null +++ b/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 00000000..89c2725b --- /dev/null +++ b/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/demos/supabase-trello/ios/Runner/Base.lproj/LaunchScreen.storyboard b/demos/supabase-trello/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..f2e259c7 --- /dev/null +++ b/demos/supabase-trello/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/supabase-trello/ios/Runner/Base.lproj/Main.storyboard b/demos/supabase-trello/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f3c28516 --- /dev/null +++ b/demos/supabase-trello/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/supabase-trello/ios/Runner/Info.plist b/demos/supabase-trello/ios/Runner/Info.plist new file mode 100644 index 00000000..22057481 --- /dev/null +++ b/demos/supabase-trello/ios/Runner/Info.plist @@ -0,0 +1,51 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Trelloappclone Flutter + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + trelloappclone_flutter + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/demos/supabase-trello/ios/Runner/Runner-Bridging-Header.h b/demos/supabase-trello/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 00000000..308a2a56 --- /dev/null +++ b/demos/supabase-trello/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/demos/supabase-trello/ios/RunnerTests/RunnerTests.swift b/demos/supabase-trello/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..86a7c3b1 --- /dev/null +++ b/demos/supabase-trello/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/demos/supabase-trello/lib/features/aboutboard/presentation/index.dart b/demos/supabase-trello/lib/features/aboutboard/presentation/index.dart new file mode 100644 index 00000000..4f089aea --- /dev/null +++ b/demos/supabase-trello/lib/features/aboutboard/presentation/index.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/constant.dart'; + +class AboutBoard extends StatefulWidget { + const AboutBoard({super.key}); + + @override + State createState() => _AboutBoardState(); +} + +class _AboutBoardState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("About this board"), + centerTitle: false, + ), + body: const Padding( + padding: EdgeInsets.all(20.0), + child: Column(children: [ + Text( + "Made by", + style: TextStyle(fontWeight: FontWeight.w600), + ), + ListTile( + leading: CircleAvatar(), + title: Text("Jane Doe"), + subtitle: Text("@janedoe"), + ), + Text( + "Description", + style: TextStyle(fontWeight: FontWeight.w600), + ), + Padding( + padding: EdgeInsets.only(top: 10.0), + child: Text(defaultDescription), + ) + ]), + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/activity/presentation/index.dart b/demos/supabase-trello/lib/features/activity/presentation/index.dart new file mode 100644 index 00000000..6a15a08c --- /dev/null +++ b/demos/supabase-trello/lib/features/activity/presentation/index.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; +import 'package:trelloappclone_flutter/models/activity.dart'; +import 'package:trelloappclone_flutter/models/card.dart'; + +import '../../../utils/service.dart'; + +class Activities extends StatefulWidget { + final Cardlist crd; + const Activities(this.crd, {super.key}); + + @override + State createState() => _ActivitiesState(); +} + +class _ActivitiesState extends State with Service { + List activities = []; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + initialData: activities, + future: getActivities(widget.crd), + builder: + (BuildContext context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + List children = snapshot.data as List; + + if (children.isNotEmpty) { + return ListView( + shrinkWrap: true, children: buildWidget(children)); + } + } + return const SizedBox.shrink(); + }); + } + + List buildWidget(List activities) { + List tiles = []; + + for (int i = 0; i < activities.length; i++) { + tiles.add(ActivityTile(activity: activities[i].description)); + } + return tiles; + } +} + +class ActivityTile extends StatefulWidget { + final String activity; + const ActivityTile({required this.activity, super.key}); + + @override + State createState() => _ActivityTileState(); +} + +class _ActivityTileState extends State { + @override + Widget build(BuildContext context) { + return ListTile( + leading: const CircleAvatar( + backgroundColor: brandColor, + ), + title: Text(widget.activity), + subtitle: const Text("01 Jan 2023 at 1:11 am"), + ); + } +} diff --git a/demos/supabase-trello/lib/features/archivedcards/presentation/index.dart b/demos/supabase-trello/lib/features/archivedcards/presentation/index.dart new file mode 100644 index 00000000..5702546d --- /dev/null +++ b/demos/supabase-trello/lib/features/archivedcards/presentation/index.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +import '../../../utils/color.dart'; + +class ArchivedCards extends StatefulWidget { + const ArchivedCards({super.key}); + + @override + State createState() => _ArchivedCardsState(); +} + +class _ArchivedCardsState extends State { + bool select = false; + int selected = 0; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: (select) + ? IconButton( + onPressed: () {}, + icon: const Icon( + Icons.close, + size: 30, + ), + ) + : null, + title: Text((select) ? "&selected selected" : "Archived cards"), + centerTitle: false, + actions: [ + (select) + ? TextButton( + onPressed: () {}, + child: const Text( + "SEND TO BOARD", + style: TextStyle(color: whiteShade), + )) + : IconButton( + onPressed: () {}, + icon: const Icon(Icons.check_circle_outline)) + ], + ), + body: const Center( + child: Text("No archived cards"), + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/archivedlists/presentation/index.dart b/demos/supabase-trello/lib/features/archivedlists/presentation/index.dart new file mode 100644 index 00000000..76f5181c --- /dev/null +++ b/demos/supabase-trello/lib/features/archivedlists/presentation/index.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; + +class ArchivedLists extends StatefulWidget { + const ArchivedLists({super.key}); + + @override + State createState() => _ArchivedListsState(); +} + +class _ArchivedListsState extends State { + bool select = false; + int selected = 0; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: (select) + ? IconButton( + onPressed: () {}, + icon: const Icon( + Icons.close, + size: 30, + ), + ) + : null, + title: Text((select) ? "&selected selected" : "Archived lists"), + centerTitle: false, + actions: [ + (select) + ? TextButton( + onPressed: () {}, + child: const Text( + "SEND TO BOARD", + style: TextStyle(color: whiteShade), + )) + : IconButton( + onPressed: () {}, + icon: const Icon(Icons.check_circle_outline)) + ], + ), + body: const Center( + child: Text("No archived lists"), + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/board/domain/board_arguments.dart b/demos/supabase-trello/lib/features/board/domain/board_arguments.dart new file mode 100644 index 00000000..7b7720d5 --- /dev/null +++ b/demos/supabase-trello/lib/features/board/domain/board_arguments.dart @@ -0,0 +1,9 @@ +import 'package:trelloappclone_flutter/models/board.dart'; +import 'package:trelloappclone_flutter/models/workspace.dart'; + +class BoardArguments { + final Board board; + final Workspace workspace; + + BoardArguments(this.board, this.workspace); +} diff --git a/demos/supabase-trello/lib/features/board/presentation/boarditemobject.dart b/demos/supabase-trello/lib/features/board/presentation/boarditemobject.dart new file mode 100644 index 00000000..62f5b5b4 --- /dev/null +++ b/demos/supabase-trello/lib/features/board/presentation/boarditemobject.dart @@ -0,0 +1,13 @@ +import 'package:trelloappclone_flutter/models/card_label.dart'; + +class BoardItemObject { + String? title; + bool? hasDescription; + List? cardLabels; + + BoardItemObject({this.title, this.hasDescription, this.cardLabels}) { + title ??= ""; + hasDescription ??= false; + cardLabels ??= []; + } +} diff --git a/demos/supabase-trello/lib/features/board/presentation/boardlistobject.dart b/demos/supabase-trello/lib/features/board/presentation/boardlistobject.dart new file mode 100644 index 00000000..1a234877 --- /dev/null +++ b/demos/supabase-trello/lib/features/board/presentation/boardlistobject.dart @@ -0,0 +1,13 @@ +import 'boarditemobject.dart'; + +class BoardListObject { + String? title; + String? listId; + List? items; + + BoardListObject({this.title, this.listId, this.items}) { + listId ??= "0"; + title ??= ""; + items ??= []; + } +} diff --git a/demos/supabase-trello/lib/features/board/presentation/index.dart b/demos/supabase-trello/lib/features/board/presentation/index.dart new file mode 100644 index 00000000..cf9b7309 --- /dev/null +++ b/demos/supabase-trello/lib/features/board/presentation/index.dart @@ -0,0 +1,488 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/features/carddetails/domain/card_detail_arguments.dart'; +import 'package:trelloappclone_flutter/features/carddetails/presentation/index.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; +import 'package:trelloappclone_flutter/widgets/thirdparty/board_item.dart'; +import 'package:trelloappclone_flutter/widgets/thirdparty/board_list.dart'; +import 'package:trelloappclone_flutter/widgets/thirdparty/boardview.dart'; +import 'package:trelloappclone_flutter/widgets/thirdparty/boardview_controller.dart'; +import 'package:trelloappclone_flutter/models/listboard.dart'; +import 'package:trelloappclone_flutter/models/card.dart'; + +import '../../../main.dart'; +import '../../../utils/config.dart'; +import '../../../utils/service.dart'; +import '../../../utils/widgets.dart'; +import '../domain/board_arguments.dart'; +import 'boarditemobject.dart'; +import 'boardlistobject.dart'; + +class BoardScreen extends StatefulWidget { + const BoardScreen({super.key}); + + @override + State createState() => _BoardScreenState(); + + static const routeName = '/board'; +} + +class _BoardScreenState extends State with Service { + BoardViewController boardViewController = BoardViewController(); + bool showCard = false; + bool show = false; + List lists = []; + final TextEditingController nameController = TextEditingController(); + Map textEditingControllers = {}; + Map showtheCard = {}; + int selectedList = 0; + int selectedCard = 0; + late double width; + + @override + Widget build(BuildContext context) { + width = MediaQuery.of(context).size.width * 0.7; + final args = ModalRoute.of(context)!.settings.arguments as BoardArguments; + trello.setSelectedBoard(args.board); + trello.setSelectedWorkspace(args.workspace); + + // ignore: deprecated_member_use + return WillPopScope( + onWillPop: () async { + Navigator.pushNamed(context, "/home"); + return false; + }, + child: Scaffold( + appBar: (!show && !showCard) + ? AppBar( + backgroundColor: brandColor, + centerTitle: false, + title: Text(args.board.name), + actions: [ + IconButton( + onPressed: () { + Navigator.pushNamed(context, '/boardmenu'); + }, + icon: const Icon(Icons.more_horiz)) + ], + ) + : AppBar( + leading: IconButton( + onPressed: () { + setState(() { + nameController.clear(); + textEditingControllers[selectedList]!.clear(); + show = false; + showCard = false; + showtheCard[selectedCard] = false; + }); + }, + icon: const Icon(Icons.close)), + title: Text((show) ? "Add list" : "Add card"), + centerTitle: false, + actions: [ + IconButton( + onPressed: () { + if (show) { + addList(Listboard( + id: randomUuid(), + workspaceId: args.workspace.id, + boardId: args.board.id, + userId: trello.user.id, + name: nameController.text, + order: trello.lstbrd.length)); + nameController.clear(); + setState(() { + show = false; + }); + } else { + addCard(Cardlist( + id: randomUuid(), + workspaceId: args.workspace.id, + listId: trello.lstbrd[selectedList].id, + userId: trello.user.id, + name: + textEditingControllers[selectedList]!.text, + rank: + trello.lstbrd[selectedList].cards!.length)); + textEditingControllers[selectedList]!.clear(); + setState(() { + showCard = false; + showtheCard[selectedCard] = false; + }); + } + }, + icon: const Icon(Icons.check)) + ], + ), + body: Padding( + padding: const EdgeInsets.all(10.0), + child: StreamBuilder( + stream: getListsByBoardStream(args.board), + builder: (BuildContext context, + AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + List listBoards = + snapshot.data as List; + return BoardView( + lists: loadBoardView(listBoards), + boardViewController: boardViewController, + ); + } + return const SizedBox.shrink(); + })), + )); + } + + Widget buildBoardItem( + BoardItemObject itemObject, List data) { + return BoardItem( + onStartDragItem: (listIndex, itemIndex, state) {}, + onDropItem: (listIndex, itemIndex, oldListIndex, oldItemIndex, state) { + // if listIndex is null, then item was dropped outside of list reset the state + if (listIndex == null) { + return; + } + + if (itemIndex == null || itemIndex > data[listIndex].items!.length) { + return; + } + + // Move item to new list + var item = data[oldListIndex!].items?[oldItemIndex!]; + data[oldListIndex].items!.removeAt(oldItemIndex!); + data[listIndex].items!.insert(itemIndex, item!); + + var card = trello.lstbrd[oldListIndex].cards![oldItemIndex]; + + // update card listId + card.listId = trello.lstbrd[listIndex].id; + updateCard(card); + + trello.lstbrd[oldListIndex].cards!.removeAt(oldItemIndex); + trello.lstbrd[listIndex].cards!.insert(itemIndex, card); + + // reset rank based on index + trello.lstbrd[listIndex].cards!.asMap().forEach((index, card) { + card.rank = index; + updateCard(card); + }); + }, + onTapItem: (listIndex, itemIndex, state) { + Navigator.pushNamed(context, CardDetails.routeName, + arguments: CardDetailArguments( + trello.lstbrd[listIndex].cards![itemIndex], + trello.selectedBoard, + trello.lstbrd[listIndex])) + .then((value) => setState(() {})); + }, + item: Card( + color: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + child: Column(children: [ + Padding( + padding: const EdgeInsets.fromLTRB(10, 8, 8, 8), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + itemObject.title!, + ), + ), + ), + Wrap( + children: [ + // Add a horizontal space + const SizedBox(width: 0), + // Example labels with colored Chips + ...itemObject.cardLabels!.map((cardLabel) => Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4, vertical: 2), // Horizontal margin + child: LabelDiplay( + color: trello.selectedBoard.boardLabels! + .firstWhere((boardLabel) => + boardLabel.id == cardLabel.boardLabelId) + .color, + label: trello.selectedBoard.boardLabels! + .firstWhere((boardLabel) => + boardLabel.id == cardLabel.boardLabelId) + .title))), + ], + ), + //Add icon to the column if card has description + if (itemObject.hasDescription!) + const Padding( + padding: EdgeInsets.fromLTRB(8, 2, 8, 8), + child: Align( + alignment: Alignment.centerLeft, + child: Icon(Icons.description, size: 16), + ), + ), + ]))); + } + + Widget _createBoardList( + BoardListObject list, List data, int index) { + List items = []; + for (int i = 0; i < list.items!.length; i++) { + items.insert(i, buildBoardItem(list.items![i], data) as BoardItem); + } + + textEditingControllers.putIfAbsent(index, () => TextEditingController()); + showtheCard.putIfAbsent(index, () => false); + + items.insert( + list.items!.length, + BoardItem( + onTapItem: (listIndex, itemIndex, state) { + setState(() { + selectedList = listIndex; + selectedCard = index; + showCard = true; + showtheCard[index] = true; + }); + }, + item: (!showtheCard[index]!) + ? ListTile( + leading: const Text.rich(TextSpan( + children: [ + WidgetSpan( + child: Icon( + Icons.add, + size: 19, + color: whiteShade, + )), + WidgetSpan( + child: SizedBox( + width: 5, + ), + ), + TextSpan( + text: "Add card", + style: TextStyle(color: whiteShade)), + ], + )), + trailing: IconButton( + icon: const Icon( + Icons.image, + color: whiteShade, + ), + onPressed: () {}, + ), + ) + : Padding( + padding: const EdgeInsets.all(10.0), + child: TextField( + controller: textEditingControllers[index], + decoration: const InputDecoration(hintText: "Card name"), + ), + ), + )); + + return BoardList( + onStartDragList: (listIndex) {}, + onTapList: (listIndex) async {}, + onDropList: (listIndex, oldListIndex) { + var tmpList = data[oldListIndex!]; + + data.removeAt(oldListIndex); + data.insert(listIndex!, tmpList); + + updateListOrder(tmpList.listId!, listIndex); + + var movedList = trello.lstbrd[oldListIndex]; + + trello.lstbrd.removeAt(oldListIndex); + trello.lstbrd.insert(listIndex, movedList); + + // reset rank based on index + trello.lstbrd.asMap().forEach((index, list) { + updateListOrder(list.id, index); + }); + }, + headerBackgroundColor: brandColor, + backgroundColor: brandColor, + header: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(5), + child: ListTile( + leading: SizedBox( + width: 180, + child: Text( + overflow: TextOverflow.ellipsis, + softWrap: false, + maxLines: 2, + list.title!, + style: const TextStyle( + fontSize: 16, fontWeight: FontWeight.w500), + ), + ), + trailing: PopupMenuButton( + child: const Icon(Icons.more_vert), + itemBuilder: (BuildContext context) => + >[ + PopupMenuItem( + child: ListTile( + enabled: false, + title: Text(listMenu[1]), + ), + ), + PopupMenuItem( + child: ListTile( + enabled: false, + title: Text(listMenu[2]), + ), + ), + PopupMenuItem( + child: ListTile( + enabled: false, + title: Text(listMenu[3]), + ), + ), + const PopupMenuItem( + child: Divider( + height: 1, + thickness: 1, + )), + PopupMenuItem( + child: ListTile( + enabled: false, + title: Text(listMenu[4]), + trailing: + const Icon(Icons.keyboard_arrow_right), + ), + ), + const PopupMenuItem( + child: Divider( + height: 1, + thickness: 1, + )), + PopupMenuItem( + child: ListTile( + enabled: false, + title: Text(listMenu[5]), + ), + ), + PopupMenuItem( + child: ListTile( + title: Text(listMenu[6]), + onTap: () { + archiveCardsInList(trello.lstbrd[index]) + .then((numCardsArchived) { + // ignore: use_build_context_synchronously + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 2), + margin: EdgeInsets.only( + // ignore: use_build_context_synchronously + bottom: MediaQuery.of(context) + .size + .height * + 0.1, // 10% from bottom + right: 20, + left: 20, + ), + content: Row( + children: [ + const Icon(Icons.archive_outlined, + color: brandColor), + const SizedBox(width: 12), + Text( + '$numCardsArchived Cards Archived'), + ], + ), + behavior: SnackBarBehavior.floating, + ), + ); + // ignore: use_build_context_synchronously + Navigator.of(context).pop(); + }); + }, + ), + ), + PopupMenuItem( + child: ListTile( + enabled: false, + title: Text(listMenu[7]), + ), + ), + ]), + ))), + ], + items: items, + ); + } + + List generateBoardListObject(List lists) { + final List listData = []; + + for (int i = 0; i < lists.length; i++) { + listData.add(BoardListObject( + title: lists[i].name, + listId: lists[i].id, + items: generateBoardItemObject(lists[i].cards!))); + } + + return listData; + } + + List generateBoardItemObject(List crds) { + final List items = []; + for (int i = 0; i < crds.length; i++) { + items.add(BoardItemObject( + title: crds[i].name, + cardLabels: crds[i].cardLabels, + hasDescription: (crds[i].description != null) ? true : false)); + } + return items; + } + + // ignore: non_constant_identifier_names + List loadBoardView(List Listboards) { + List data = generateBoardListObject(Listboards); + lists = []; + + for (int i = 0; i < data.length; i++) { + lists.add(_createBoardList(data[i], data, i) as BoardList); + } + + lists.insert( + data.length, + BoardList( + items: [ + BoardItem( + item: GestureDetector( + onTap: () { + setState(() { + show = true; + }); + }, + child: Container( + alignment: Alignment.center, + width: width, + height: 50, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + color: brandColor, + ), + child: (!show) + ? const Text( + "Add list", + style: TextStyle(color: whiteShade), + ) + : Padding( + padding: const EdgeInsets.all(10.0), + child: TextField( + controller: nameController, + decoration: + const InputDecoration(hintText: "List name"), + ), + )), + )) + ], + )); + return lists; + } +} diff --git a/demos/supabase-trello/lib/features/boardbackground/presentation/index.dart b/demos/supabase-trello/lib/features/boardbackground/presentation/index.dart new file mode 100644 index 00000000..6b08897a --- /dev/null +++ b/demos/supabase-trello/lib/features/boardbackground/presentation/index.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/config.dart'; +import '../../../main.dart'; + +class BoardBackground extends StatefulWidget { + const BoardBackground({super.key}); + + @override + State createState() => _BoardBackgroundState(); +} + +class _BoardBackgroundState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Board background"), + centerTitle: false, + ), + body: GridView.builder( + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + childAspectRatio: 1, + crossAxisSpacing: 3, + mainAxisSpacing: 20), + itemCount: backgrounds.length, + itemBuilder: (BuildContext cxt, index) { + return GestureDetector( + onTap: () { + setState(() { + trello.setSelectedBg(backgrounds[index]); + }); + }, + child: Stack( + children: [ + Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: Color(int.parse( + backgrounds[index].substring(1, 7), + radix: 16) + + 0xff000000), + borderRadius: BorderRadius.circular(5)), + ), + (backgrounds[index] == trello.selectedBackground) + ? const Center( + child: Icon( + Icons.check, + color: Colors.white, + size: 50, + ), + ) + : const SizedBox.shrink() + ], + )); + }), + ); + } +} diff --git a/demos/supabase-trello/lib/features/boardmenu/presentation/index.dart b/demos/supabase-trello/lib/features/boardmenu/presentation/index.dart new file mode 100644 index 00000000..82badf5b --- /dev/null +++ b/demos/supabase-trello/lib/features/boardmenu/presentation/index.dart @@ -0,0 +1,202 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/features/visibility/presentation/index.dart'; +import 'package:trelloappclone_flutter/main.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; + +class BoardMenu extends StatefulWidget { + const BoardMenu({super.key}); + + @override + State createState() => _BoardMenuState(); +} + +class _BoardMenuState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: const Icon(Icons.close), + ), + title: const Text("Board menu"), + centerTitle: false, + ), + body: SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(20.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + decoration: const BoxDecoration( + color: brandColor, + borderRadius: BorderRadius.all(Radius.circular(10.0))), + child: IconButton( + onPressed: () {}, + icon: const Icon( + Icons.star_border, + size: 30, + ), + ), + ), + Container( + decoration: const BoxDecoration( + color: brandColor, + borderRadius: BorderRadius.all(Radius.circular(10.0))), + child: IconButton( + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return const BoardVisibility(); + }); + }, + icon: const Icon( + Icons.people, + size: 30, + ), + ), + ), + Container( + decoration: const BoxDecoration( + color: brandColor, + borderRadius: BorderRadius.all(Radius.circular(10.0))), + child: IconButton( + onPressed: () { + Navigator.pushNamed(context, "/copyboard"); + }, + icon: const Icon( + Icons.copy, + size: 30, + ), + ), + ), + Container( + decoration: const BoxDecoration( + color: brandColor, + borderRadius: BorderRadius.all(Radius.circular(10.0))), + child: IconButton( + onPressed: () { + Navigator.pushNamed(context, "/boardsettings"); + }, + icon: const Icon( + Icons.more_horiz, + size: 30, + ), + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.only(top: 10.0), + child: ListTile( + tileColor: whiteShade, + leading: const Icon(Icons.person_outline), + title: const Padding( + padding: EdgeInsets.only(top: 15.0, bottom: 15.0), + child: Text("Members"), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 18.0), + child: InkWell( + onTap: () { + Navigator.pushNamed(context, '/members'); + }, + child: Row( + children: buildMemberAvatars(), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 15.0), + child: SizedBox( + height: 37, + width: MediaQuery.of(context).size.width * 0.7, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: brandColor), + onPressed: () { + Navigator.pushNamed(context, "/invitemember"); + }, + child: const Text("Invite to workspace"), + ), + ), + ) + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 15.0), + child: Container( + color: whiteShade, + child: ListTile( + leading: const Icon(Icons.info_outline), + title: const Text("About this board"), + onTap: () { + Navigator.pushNamed(context, '/aboutboard'); + }, + ), + ), + ), + // Padding( + // padding: const EdgeInsets.only(top: 15.0), + // child: Container( + // color: whiteShade, + // child: ListTile( + // leading: const Icon(Icons.rocket), + // title: const Text("Power-Ups"), + // onTap: () { + // Navigator.pushNamed(context, '/powerups'); + // }, + // ), + // )), + // Padding( + // padding: const EdgeInsets.only(top: 15.0), + // child: Container( + // color: whiteShade, + // child: ListTile( + // leading: const Icon(Icons.push_pin_outlined), + // title: const Text("Pin to home screen"), + // onTap: () {}, + // ), + // )), + // const Padding( + // padding: EdgeInsets.all(15.0), + // child: Text( + // "Activity", + // style: TextStyle(fontWeight: FontWeight.bold), + // ), + // ), + // //TODO: figure out what is going on here + // Activities(Cardlist( + // id: "todo", workspaceId: trello.selectedWorkspace.id, listId: "todo", userId: trello.user.id, name: "")) + ], + )), + ); + } + + List buildMemberAvatars() { + List avatars = []; + + trello.selectedWorkspace.members?.forEach((member) { + avatars.add(CircleAvatar( + backgroundColor: brandColor, + child: Text(member.name[0].toUpperCase()), + )); + avatars.add(const SizedBox( + width: 4, + )); + }); + return avatars; + } +} diff --git a/demos/supabase-trello/lib/features/boardsettings/presentation/index.dart b/demos/supabase-trello/lib/features/boardsettings/presentation/index.dart new file mode 100644 index 00000000..b3826325 --- /dev/null +++ b/demos/supabase-trello/lib/features/boardsettings/presentation/index.dart @@ -0,0 +1,206 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; +import 'package:trelloappclone_flutter/utils/config.dart'; + +import '../../../utils/widgets.dart'; +import '../../closeboard/presentation/index.dart'; + +class BoardSettings extends StatefulWidget { + const BoardSettings({super.key}); + + @override + State createState() => _BoardSettingsState(); +} + +class _BoardSettingsState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text("Board settings")), + body: SingleChildScrollView( + child: Column( + children: [ + const BlueRectangle(), + Padding( + padding: const EdgeInsets.only(top: 15.0), + child: Container( + color: whiteShade, + child: const ListTile( + leading: Text("Name"), + trailing: Text("Board 1"), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 3.0), + child: Container( + color: whiteShade, + child: const ListTile( + leading: Text("Workspace"), + trailing: Text("Workspace 1"), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 3.0), + child: Container( + color: whiteShade, + child: ListTile( + leading: const Text("Background"), + trailing: ColorSquare( + bckgrd: backgrounds[0], + ), + onTap: () { + Navigator.pushNamed(context, "/boardbackground"); + }, + ), + ), + ), + // Padding( + // padding: const EdgeInsets.only(top: 3.0), + // child: Container( + // color: whiteShade, + // child: ListTile( + // leading: const Text("Enable card cover images"), + // trailing: Switch(value: true, onChanged: ((value) {})), + // ), + // ), + // ), + // Padding( + // padding: const EdgeInsets.only(top: 3.0), + // child: Container( + // color: whiteShade, + // child: ListTile( + // leading: const Text("Watch"), + // trailing: Switch(value: false, onChanged: ((value) {}))), + // ), + // ), + // Padding( + // padding: const EdgeInsets.only(top: 3.0), + // child: Container( + // color: whiteShade, + // child: ListTile( + // leading: const Text("Available offline"), + // trailing: Switch(value: false, onChanged: ((value) {}))), + // ), + // ), + // Padding( + // padding: const EdgeInsets.only(top: 3.0), + // child: Container( + // color: whiteShade, + // child: ListTile( + // leading: const Text("Edit labels"), + // onTap: () { + // showDialog( + // context: context, + // builder: (BuildContext context) { + // return const EditLabels(); + // }); + // }, + // ), + // ), + // ), + // Padding( + // padding: const EdgeInsets.only(top: 3.0), + // child: Container( + // color: whiteShade, + // child: ListTile( + // leading: const Text("Email-to-board settings"), + // onTap: () { + // Navigator.pushNamed(context, "/emailtoboard"); + // }, + // ), + // ), + // ), + // Padding( + // padding: const EdgeInsets.only(top: 3.0), + // child: Container( + // color: whiteShade, + // child: ListTile( + // leading: const Text("Archived cards"), + // onTap: () { + // Navigator.pushNamed(context, "/archivedcards"); + // }, + // ), + // ), + // ), + // Padding( + // padding: const EdgeInsets.only(top: 3.0), + // child: Container( + // color: whiteShade, + // child: ListTile( + // leading: const Text("Archived lists"), + // onTap: () { + // Navigator.pushNamed(context, "/archivedlists"); + // }, + // ), + // ), + // ), + Padding( + padding: const EdgeInsets.only(top: 15.0), + child: Container( + color: whiteShade, + child: const ListTile( + leading: Text("Visibility"), + trailing: Text("Public"), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 3.0), + child: Container( + color: whiteShade, + child: const ListTile( + leading: Text("Commenting"), + trailing: Text("Members"), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 3.0), + child: Container( + color: whiteShade, + child: const ListTile( + leading: Text("Adding members"), + trailing: Text("Members"), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 15.0), + child: Container( + color: whiteShade, + child: ListTile( + leading: const Text("Self join"), + trailing: Switch( + value: true, + onChanged: ((value) {}), + )), + ), + ), + const Padding( + padding: EdgeInsets.only(top: 10.0), + child: + Text("Any Workspace member can edit and join the board")), + Padding( + padding: const EdgeInsets.only(top: 15.0, bottom: 50), + child: Container( + color: whiteShade, + child: ListTile( + leading: const Text("Close board"), + onTap: () { + showDialog( + context: context, + builder: (BuildContext context) { + return const CloseBoard(); + }); + }, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/carddetails/domain/card_detail_arguments.dart b/demos/supabase-trello/lib/features/carddetails/domain/card_detail_arguments.dart new file mode 100644 index 00000000..b76dfd32 --- /dev/null +++ b/demos/supabase-trello/lib/features/carddetails/domain/card_detail_arguments.dart @@ -0,0 +1,11 @@ +import 'package:trelloappclone_flutter/models/board.dart'; +import 'package:trelloappclone_flutter/models/card.dart'; +import 'package:trelloappclone_flutter/models/listboard.dart'; + +class CardDetailArguments { + final Cardlist crd; + final Board brd; + final Listboard lst; + + CardDetailArguments(this.crd, this.brd, this.lst); +} diff --git a/demos/supabase-trello/lib/features/carddetails/presentation/index.dart b/demos/supabase-trello/lib/features/carddetails/presentation/index.dart new file mode 100644 index 00000000..ef820c40 --- /dev/null +++ b/demos/supabase-trello/lib/features/carddetails/presentation/index.dart @@ -0,0 +1,376 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/features/activity/presentation/index.dart'; +import 'package:trelloappclone_flutter/models/checklist.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; +import 'package:trelloappclone_flutter/main.dart'; + +import '../../../utils/service.dart'; +import '../../../utils/widgets.dart'; +import '../../editlabels/presentation/index.dart'; +import '../../viewmembers/presentation/index.dart'; +import '../domain/card_detail_arguments.dart'; + +class CardDetails extends StatefulWidget { + const CardDetails({super.key}); + + @override + State createState() => _CardDetailsState(); + + static const routeName = '/carddetail'; +} + +class _CardDetailsState extends State with Service { + final TextEditingController descriptionController = TextEditingController(); + final TextEditingController nameController = TextEditingController(); + final TextEditingController checklistController = TextEditingController(); + bool showChecklist = false; + bool addCardDescription = false; + bool editCardName = false; + Map checked = {}; + + @override + Widget build(BuildContext context) { + final args = + ModalRoute.of(context)!.settings.arguments as CardDetailArguments; + + trello.setSelectedCard(args.crd); + descriptionController.text = args.crd.description ?? " "; + nameController.text = args.crd.name; + + return Scaffold( + appBar: (showChecklist || addCardDescription || editCardName) + ? AppBar( + leading: IconButton( + onPressed: () { + setState(() { + showChecklist = false; + addCardDescription = false; + editCardName = false; + }); + }, + icon: const Icon(Icons.close, size: 30), + ), + title: Text(() { + if (showChecklist) { + return "Add Checklist"; + } else if (addCardDescription) { + return "Add card description"; + } else if (editCardName) { + return "Edit card name"; + } else { + return ""; + } + }()), + actions: [ + IconButton( + icon: const Icon(Icons.check), + onPressed: () { + if (showChecklist) { + createChecklist(Checklist( + id: randomUuid(), + workspaceId: args.crd.workspaceId, + cardId: args.crd.id, + name: checklistController.text, + status: false)); + checklistController.clear(); + setState(() { + showChecklist = false; + }); + } else if (addCardDescription || editCardName) { + if (addCardDescription && + descriptionController.text.isNotEmpty) { + args.crd.description = descriptionController.text; + } + + if (editCardName && nameController.text.isNotEmpty) { + args.crd.name = nameController.text; + } + + updateCard(args.crd); + descriptionController.clear(); + nameController.clear(); + setState(() { + addCardDescription = false; + editCardName = false; + }); + } + }, + ) + ]) + : AppBar( + leading: IconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: const Icon(Icons.close, size: 30), + ), + actions: [ + PopupMenuButton( + itemBuilder: (context) { + return [ + PopupMenuItem( + onTap: () => + WidgetsBinding.instance.addPostFrameCallback((_) { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text('Delete Card'), + content: const Text( + 'Are you sure you want to delete this card?'), + actions: [ + TextButton( + onPressed: () => + Navigator.pop(context, 'Cancel'), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => { + deleteCard(args.crd), + // Remove popup + Navigator.pop(context, 'Delete'), + // Go one view back + Navigator.pop(context, 'Delete'), + }, + child: const Text('Delete'), + ), + ], + )); + }), + value: const Text("Delete Card"), + child: const ListTile( + leading: Icon(Icons.delete), + title: Text( + "Delete Card", + ), + ), + ), + ]; + }, + ) + // IconButton( + // icon: const Icon(Icons.more_vert), + // onPressed: () {}, + // ) + ]), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(10.0), + child: + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + ListTile( + title: TextField( + style: const TextStyle( + fontSize: 20.0, // Set your desired font size here + ), + controller: nameController, + onTap: () { + setState(() { + editCardName = true; + }); + }, + decoration: const InputDecoration(hintText: "Edit card name"), + ), + ), + RichText( + text: TextSpan( + text: args.brd.name, + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: themeColor), + children: [ + const TextSpan( + text: ' in list ', style: TextStyle(fontSize: 12)), + TextSpan(text: args.lst.name) + ])), + const Padding( + padding: EdgeInsets.only(top: 8.0, bottom: 8.0), + child: Text( + "Quick actions", + style: TextStyle(fontWeight: FontWeight.w600), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: Row( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width * 0.4, + child: ElevatedButton.icon( + onPressed: () { + setState(() { + showChecklist = true; + }); + }, + label: const Text("Add Checklist"), + icon: const CircleAvatar( + backgroundColor: brandColor, + radius: 15, + child: Icon(Icons.checklist), + ), + ), + ), + const Spacer(), + SizedBox( + width: MediaQuery.of(context).size.width * 0.4, + child: ElevatedButton.icon( + onPressed: null, + label: const Text("Add Attachment"), + icon: const CircleAvatar( + backgroundColor: brandColor, + radius: 15, + child: Icon(Icons.attachment), + ), + ), + ) + ], + ), + ), + ListTile( + leading: const Icon(Icons.short_text), + title: TextField( + controller: descriptionController, + keyboardType: TextInputType.multiline, + minLines: 1, + maxLines: 1024, + onTap: () { + setState(() { + addCardDescription = true; + }); + }, + decoration: + const InputDecoration(hintText: "Add card description"), + ), + ), + ListTile( + leading: const Icon(Icons.label), + title: Row( + children: [ + const Text("Labels"), + // Add a horizontal space + const SizedBox(width: 8), + // Example labels with colored Chips + ...trello.selectedCard!.cardLabels!.map( + (cardLabel) => Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4), // Horizontal margin + child: LabelDiplay( + color: trello.selectedBoard.boardLabels! + .firstWhere((boardLabel) => + boardLabel.id == cardLabel.boardLabelId) + .color, + label: trello.selectedBoard.boardLabels! + .firstWhere((boardLabel) => + boardLabel.id == cardLabel.boardLabelId) + .title)), + ), + ], + ), + onTap: () { + final result = showDialog( + context: context, + builder: (BuildContext context) { + return EditLabels(cardId: args.crd.id); + }); + + result.then((value) { + setState(() {}); + }); + }, + ), + ListTile( + leading: const Icon(Icons.person), + title: const Text("Members"), + onTap: () { + showDialog( + context: context, + builder: (BuildContext context) { + return const ViewMembers(); + }); + }, + ), + ListTile( + leading: const Icon(Icons.date_range_outlined), + title: const Text("Start date"), + onTap: () {}, + ), + ListTile( + leading: const Text("Checklist"), + trailing: IconButton( + onPressed: () { + setState(() { + deleteChecklist(args.crd); + }); + }, + icon: const Icon(Icons.delete)), + ), + FutureBuilder( + future: getChecklists(args.crd), + builder: ((context, snapshot) { + if (snapshot.hasData) { + List children = snapshot.data as List; + + if (children.isNotEmpty) { + return Column(children: buildChecklists(children)); + } + } + return const SizedBox.shrink(); + })), + Visibility( + visible: showChecklist, + child: TextField( + controller: checklistController, + ), + ), + const Text("Activity"), + Activities(args.crd) + ]), + ), + ), + //TODO: Add this back in when we get comments working properly + // bottomNavigationBar: Container( + // padding: const EdgeInsets.only(bottom: 5.0), + // color: whiteShade, + // width: MediaQuery.of(context).size.width * 0.8, + // height: 80, + // child: Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ + // const CircleAvatar(), + // SizedBox( + // width: MediaQuery.of(context).size.width * 0.7, + // child: TextField( + // decoration: InputDecoration( + // hintText: "Add comment", + // suffix: IconButton( + // onPressed: () {}, icon: const Icon(Icons.send))), + // ), + // ), + // IconButton(onPressed: () {}, icon: const Icon(Icons.attachment)) + // ]), + // ), + ); + } + + List buildChecklists(List chcklst) { + List lists = []; + + for (int i = 0; i < chcklst.length; i++) { + checked.putIfAbsent(i, () => false); + checked[i] = chcklst[i].status; + lists.add( + CheckboxListTile( + title: Text(chcklst[i].name), + value: checked[i], + onChanged: (bool? value) { + setState(() { + checked[i] = value!; + }); + chcklst[i].status = value!; + updateChecklist(chcklst[i]); + }, + ), + ); + } + + return lists; + } +} diff --git a/demos/supabase-trello/lib/features/closeboard/presentation/index.dart b/demos/supabase-trello/lib/features/closeboard/presentation/index.dart new file mode 100644 index 00000000..500b196e --- /dev/null +++ b/demos/supabase-trello/lib/features/closeboard/presentation/index.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; + +class CloseBoard extends StatefulWidget { + const CloseBoard({super.key}); + + @override + State createState() => _CloseBoardState(); +} + +class _CloseBoardState extends State { + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text("Board 1 is now closed"), + content: SizedBox( + height: 100, + child: Column( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width * 0.7, + child: ElevatedButton( + onPressed: () {}, child: const Text("Re-open")), + ), + SizedBox( + width: MediaQuery.of(context).size.width * 0.7, + child: OutlinedButton( + onPressed: () {}, + child: const Text( + "Delete", + style: TextStyle(color: dangerColor), + )), + ) + ], + ), + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/copyboard/presentation/index.dart b/demos/supabase-trello/lib/features/copyboard/presentation/index.dart new file mode 100644 index 00000000..af2c68eb --- /dev/null +++ b/demos/supabase-trello/lib/features/copyboard/presentation/index.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; + +import '../../../utils/color.dart'; +import '../../../utils/constant.dart'; + +class CopyBoard extends StatefulWidget { + const CopyBoard({super.key}); + + @override + State createState() => _CopyBoardState(); +} + +class _CopyBoardState extends State { + final TextEditingController nameController = TextEditingController(); + String? dropdownValue; + List workspaces = []; + Map? visibilityDropdownValue; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: const Icon( + Icons.close, + size: 30, + )), + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + controller: nameController, + decoration: const InputDecoration( + border: UnderlineInputBorder(), labelText: "Board name"), + ), + const Text("Workspace"), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: DropdownButton( + isExpanded: true, + value: dropdownValue, + icon: const Icon(Icons.keyboard_arrow_down), + elevation: 16, + style: const TextStyle(color: brandColor), + underline: Container( + height: 2, + color: brandColor, + ), + onChanged: (String? value) { + // This is called when the user selects an item. + setState(() { + dropdownValue = value!; + }); + }, + items: + workspaces.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + ), + const Text("Visibility"), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: DropdownButton>( + hint: const Text("Visibility"), + isExpanded: true, + value: visibilityDropdownValue, + icon: const Icon(Icons.keyboard_arrow_down), + elevation: 16, + style: const TextStyle(color: brandColor), + underline: Container( + height: 2, + color: brandColor, + ), + onChanged: (Map? value) { + setState(() { + visibilityDropdownValue = value!; + }); + }, + items: visibilityConfigurations + .map>>( + (Map value) { + return DropdownMenuItem>( + value: value, + child: Text(value["type"]!), + ); + }).toList(), + ), + ), + SwitchListTile( + value: false, + onChanged: ((value) {}), + title: const Text("Keep cards"), + ), + const Text( + "Activities and members will not be copied to the new board", + style: TextStyle(fontSize: 12), + ) + ], + ), + ), + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/createboard/presentation/index.dart b/demos/supabase-trello/lib/features/createboard/presentation/index.dart new file mode 100644 index 00000000..ecc3b997 --- /dev/null +++ b/demos/supabase-trello/lib/features/createboard/presentation/index.dart @@ -0,0 +1,145 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/main.dart'; +import 'package:trelloappclone_flutter/models/board.dart'; +import 'package:trelloappclone_flutter/models/workspace.dart'; + +import '../../../utils/color.dart'; +import '../../../utils/constant.dart'; +import '../../../utils/service.dart'; + +class CreateBoard extends StatefulWidget { + const CreateBoard({super.key}); + + @override + State createState() => _CreateBoardState(); +} + +class _CreateBoardState extends State with Service { + final TextEditingController nameController = TextEditingController(); + Workspace? dropdownValue; + List workspaces = []; + Map? visibilityDropdownValue; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: const Icon(Icons.close)), + title: const Text("Create board"), + centerTitle: false, + ), + body: Padding( + padding: const EdgeInsets.all(30.0), + child: Column( + children: [ + TextField( + controller: nameController, + decoration: const InputDecoration(hintText: "Enter Board name"), + ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: DropdownButton( + hint: const Text("Workspace"), + isExpanded: true, + value: dropdownValue, + icon: const Icon(Icons.keyboard_arrow_down), + elevation: 16, + style: const TextStyle(color: brandColor), + underline: Container( + height: 2, + color: brandColor, + ), + onChanged: (Workspace? value) { + setState(() { + dropdownValue = value!; + }); + }, + items: trello.workspaces + .map>((Workspace value) { + return DropdownMenuItem( + value: value, + child: Text(value.name), + ); + }).toList(), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: DropdownButton>( + hint: const Text("Visibility"), + isExpanded: true, + value: visibilityDropdownValue, + icon: const Icon(Icons.keyboard_arrow_down), + elevation: 16, + style: const TextStyle(color: brandColor), + underline: Container( + height: 2, + color: brandColor, + ), + onChanged: (Map? value) { + setState(() { + visibilityDropdownValue = value!; + }); + }, + items: visibilityConfigurations + .map>>( + (Map value) { + return DropdownMenuItem>( + value: value, + child: Text(value["type"]!), + ); + }).toList(), + ), + ), + Row( + children: [ + const Text("Board backgroud"), + const Spacer(), + GestureDetector( + onTap: () { + Navigator.pushNamed(context, '/boardbackground') + .then((_) => setState(() {})); + }, + child: Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: Color(int.parse( + trello.selectedBackground.substring(1, 7), + radix: 16) + + 0xFF000000))), + ), + ], + ), + Align( + alignment: Alignment.center, + child: Container( + padding: const EdgeInsets.only(top: 10), + width: MediaQuery.of(context).size.width * 0.8, + height: 60, + child: ElevatedButton( + onPressed: () { + createBoard( + context, + Board( + id: randomUuid(), + workspaceId: dropdownValue!.id, + userId: trello.user.id, + name: nameController.text, + visibility: visibilityDropdownValue!["type"]!, + background: trello.selectedBackground)); + }, + child: const Text("Create board"), + ), + ), + ) + ], + ), + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/createcard/presentation/index.dart b/demos/supabase-trello/lib/features/createcard/presentation/index.dart new file mode 100644 index 00000000..8b730685 --- /dev/null +++ b/demos/supabase-trello/lib/features/createcard/presentation/index.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; + +import '../../../utils/color.dart'; + +class CreateCard extends StatefulWidget { + const CreateCard({super.key}); + + @override + State createState() => _CreateCardState(); +} + +class _CreateCardState extends State { + String? dropdownValue; + List boards = ["Board 1"]; + String? listdropdownvalue; + List lists = ["List 1"]; + final TextEditingController nameController = TextEditingController(); + final TextEditingController descriptionController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + onPressed: () { + Navigator.pushNamed(context, '/home'); + }, + icon: const Icon(Icons.close), + ), + title: const Text("New card"), + centerTitle: false, + actions: [IconButton(onPressed: () {}, icon: const Icon(Icons.check))], + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Board"), + DropdownButton( + isExpanded: true, + value: dropdownValue, + icon: const Icon(Icons.keyboard_arrow_down), + elevation: 16, + style: const TextStyle(color: brandColor), + underline: Container( + height: 2, + color: brandColor, + ), + onChanged: (String? value) { + // This is called when the user selects an item. + setState(() { + dropdownValue = value!; + }); + }, + items: boards.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + const Text("List"), + DropdownButton( + isExpanded: true, + value: dropdownValue, + icon: const Icon(Icons.keyboard_arrow_down), + elevation: 16, + style: const TextStyle(color: brandColor), + underline: Container( + height: 2, + color: brandColor, + ), + onChanged: (String? value) { + // This is called when the user selects an item. + setState(() { + listdropdownvalue = value!; + }); + }, + items: lists.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + Container( + color: brandColor, + margin: const EdgeInsets.all(10.0), + child: Card( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: nameController, + decoration: + const InputDecoration(hintText: "Card name"), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: descriptionController, + decoration: + const InputDecoration(hintText: "Card description"), + ), + ), + const ListTile( + leading: Icon(Icons.person_add), + title: Text("Jane Doe"), + ), + ListTile( + leading: const Icon(Icons.lock_clock), + title: const Text("Start date..."), + onTap: () {}, + ), + ListTile( + title: const Text("Due date..."), + onTap: () {}, + ), + ListTile( + leading: const Icon(Icons.attachment), + title: const Text("Attachment"), + onTap: () {}, + ), + ], + ), + ), + ) + ], + ), + )), + ); + } +} diff --git a/demos/supabase-trello/lib/features/createworkspace/presentation/index.dart b/demos/supabase-trello/lib/features/createworkspace/presentation/index.dart new file mode 100644 index 00000000..05691cf4 --- /dev/null +++ b/demos/supabase-trello/lib/features/createworkspace/presentation/index.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/constant.dart'; + +import '../../../utils/color.dart'; +import '../../../utils/service.dart'; + +class CreateWorkspace extends StatefulWidget { + const CreateWorkspace({super.key}); + + @override + State createState() => _CreateWorkspaceState(); +} + +class _CreateWorkspaceState extends State with Service { + final TextEditingController nameController = TextEditingController(); + final TextEditingController descriptionController = TextEditingController(); + Map? dropdownValue; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Create Workspace"), + centerTitle: false, + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.only(bottom: 8.0), + child: Text( + "Let's build a Workspace", + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + ), + ), + const Text( + "Boost your productivity by making it easier for everyone to access boards in one location", + style: TextStyle(fontSize: 16), + ), + TextField( + controller: nameController, + decoration: + const InputDecoration(hintText: "Enter workspace name"), + ), + const Padding( + padding: EdgeInsets.only(top: 8.0), + child: Text("Visibility"), + ), + DropdownButton>( + isExpanded: true, + value: dropdownValue, + icon: const Icon(Icons.keyboard_arrow_down), + elevation: 16, + style: const TextStyle(color: brandColor), + underline: Container( + height: 2, + color: brandColor, + ), + onChanged: (Map? value) { + // This is called when the user selects an item. + setState(() { + dropdownValue = value!; + }); + }, + items: visibilityConfigurations + .map>>( + (Map value) { + return DropdownMenuItem>( + value: value, + child: Text(value["type"]!), + ); + }).toList(), + ), + const Padding( + padding: EdgeInsets.only(top: 10.0), + child: Text("Description"), + ), + TextField( + controller: descriptionController, + maxLines: null, + minLines: 4, + ), + Align( + alignment: Alignment.center, + child: Container( + padding: const EdgeInsets.only(top: 10), + width: MediaQuery.of(context).size.width * 0.8, + height: 60, + child: ElevatedButton( + onPressed: () { + createWorkspace(context, + name: nameController.text, + description: descriptionController.text, + visibility: dropdownValue!["type"] ?? ""); + }, + child: const Text("Create"))), + ) + ], + ), + ), + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/drawer/presentation/index.dart b/demos/supabase-trello/lib/features/drawer/presentation/index.dart new file mode 100644 index 00000000..3d914278 --- /dev/null +++ b/demos/supabase-trello/lib/features/drawer/presentation/index.dart @@ -0,0 +1,135 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/features/workspace/domain/workspace_arguments.dart'; +import 'package:trelloappclone_flutter/main.dart'; +import 'package:trelloappclone_flutter/models/workspace.dart'; + +import '../../../utils/color.dart'; +import '../../../utils/service.dart'; +import '../../workspace/presentation/index.dart'; + +class CustomDrawer extends StatefulWidget { + const CustomDrawer({super.key}); + + @override + State createState() => _CustomDrawerState(); +} + +class _CustomDrawerState extends State with Service { + bool active = true; + @override + Widget build(BuildContext context) { + return Drawer( + child: ListView(children: [ + DrawerHeader( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const CircleAvatar( + backgroundColor: brandColor, + ), + Padding( + padding: const EdgeInsets.only(top: 5.0), + child: Text(trello.user.name ?? trello.user.email), + ), + Text("@${trello.user.name!.toLowerCase().replaceAll(" ", "")}"), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(trello.user.email), + IconButton( + onPressed: () { + setState(() { + active = !active; + }); + }, + icon: Icon((active) + ? Icons.keyboard_arrow_down + : Icons.keyboard_arrow_up)) + ], + ) + ], + )), + (active) + ? Column( + children: [ + ListTile( + leading: const Icon( + Icons.pages, + color: brandColor, + ), + title: const Text( + 'Boards', + style: TextStyle( + color: brandColor, fontWeight: FontWeight.bold), + ), + onTap: () { + Navigator.pushNamed(context, '/home'); + }, + ), + const Divider( + height: 2, + thickness: 2, + color: brandColor, + ), + // TODO: Show Cards assigned to logged in user + // ListTile( + // leading: const Icon(Icons.card_membership), + // title: const Text("My cards"), + // onTap: () { + // Navigator.pushNamed(context, '/mycards'); + // }, + // ), + ListTile( + leading: const Icon(Icons.settings), + enabled: false, + title: const Text("Settings"), + onTap: () { + Navigator.pushNamed(context, '/settings'); + }, + ), + ListTile( + leading: const Icon(Icons.help_outline_rounded), + enabled: false, + title: const Text("Help!"), + onTap: () {}, + ), + ListTile( + leading: const Icon(Icons.logout), + title: const Text("Log Out"), + onTap: () { + logOut(context); + Navigator.pushNamed(context, '/'); + }, + ), + ], + ) + : ListTile( + leading: const Icon(Icons.add), + title: const Text('Add account'), + onTap: () {}, + ) + ]), + ); + } + + List buildWorkspaces(List wkspcs) { + List tiles = []; + for (int i = 0; i < wkspcs.length; i++) { + tiles.add(ListTile( + leading: const Icon(Icons.people), + title: Text(wkspcs[i].name), + trailing: IconButton( + icon: const Icon(Icons.more_horiz), + onPressed: () { + Navigator.pushNamed(context, '/workspacemenu'); + }, + ), + onTap: () { + Navigator.pushNamed(context, WorkspaceScreen.routeName, + arguments: WorkspaceArguments(wkspcs[i])); + }, + )); + } + return tiles; + } +} diff --git a/demos/supabase-trello/lib/features/editlabels/presentation/index.dart b/demos/supabase-trello/lib/features/editlabels/presentation/index.dart new file mode 100644 index 00000000..df6cec5a --- /dev/null +++ b/demos/supabase-trello/lib/features/editlabels/presentation/index.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/main.dart'; +import 'package:trelloappclone_flutter/models/card_label.dart'; +import '../../../utils/service.dart'; + +class EditLabels extends StatefulWidget { + final String cardId; + + const EditLabels({super.key, required this.cardId}); + + @override + State createState() => _EditLabelsState(); +} + +class _EditLabelsState extends State with Service { + late List switchStates; // List to track the state of each switch + + @override + void initState() { + super.initState(); + // Initialize the switchStates list with default values (e.g., all false) + switchStates = + List.filled(trello.selectedBoard.boardLabels!.length, false); + // Set the switchStates list to true for each label that is already on the card + for (int i = 0; i < trello.selectedBoard.boardLabels!.length; i++) { + for (int j = 0; j < trello.selectedCard!.cardLabels!.length; j++) { + if (trello.selectedBoard.boardLabels![i].id == + trello.selectedCard!.cardLabels![j].boardLabelId) { + switchStates[i] = true; + } + } + } + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text("Edit labels"), + content: SizedBox( + height: 200, + child: Column(children: buildWidget()), + ), + ); + } + + List buildWidget() { + // Create and initialize a list of TextEditingControllers + List controllers = trello.selectedBoard.boardLabels! + .map((label) => TextEditingController(text: label.title)) + .toList(); + + List labelContainers = []; + for (int i = 0; i < trello.selectedBoard.boardLabels!.length; i++) { + labelContainers.add(Padding( + padding: const EdgeInsets.only(bottom: 5.0), + child: Container( + height: 35, + decoration: BoxDecoration( + color: Color(int.parse(trello.selectedBoard.boardLabels![i].color, + radix: 16) + + 0xFF000000), + borderRadius: BorderRadius.circular(5), + ), + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: TextField( + controller: controllers[i], + onChanged: (value) { + // Update the label title in the database + trello.selectedBoard.boardLabels![i].title = value; + updateBoardLabel(trello.selectedBoard.boardLabels![i]); + }, + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(5.0), + borderSide: BorderSide.none, + ), + filled: false, + contentPadding: + const EdgeInsets.symmetric(horizontal: 10), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Align( + alignment: Alignment.center, + child: Transform.scale( + scale: 0.75, // Adjust the scale to make the switch smaller + child: Switch( + value: switchStates[ + i], // You might want to manage this state properly + onChanged: (bool value) async { + // Handle toggle logic + if (value) { + // Add label to card via service.data + var cardLabel = await addCardLabel( + CardLabel( + id: randomUuid(), + workspaceId: trello.selectedBoard + .boardLabels![i].workspaceId, + boardLabelId: + trello.selectedBoard.boardLabels![i].id, + boardId: trello + .selectedBoard.boardLabels![i].boardId, + cardId: widget.cardId, + dateCreated: DateTime.now()), + trello.selectedBoard.boardLabels![i]); + trello.selectedCard!.cardLabels!.add(cardLabel); + } else { + // Remove label from card + deleteCardLabel(widget.cardId, + trello.selectedBoard.boardLabels![i]); + trello.selectedCard!.cardLabels!.removeWhere( + (element) => + element.boardLabelId == + trello.selectedBoard.boardLabels![i].id); + } + setState(() { + switchStates[i] = + value; // Update the state when the switch is toggled + }); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + ), + ), + ], + ), + ), + )); + } + return labelContainers; + } +} diff --git a/demos/supabase-trello/lib/features/emailtoboard/presentation/index.dart b/demos/supabase-trello/lib/features/emailtoboard/presentation/index.dart new file mode 100644 index 00000000..6e2e4225 --- /dev/null +++ b/demos/supabase-trello/lib/features/emailtoboard/presentation/index.dart @@ -0,0 +1,135 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; + +class EmailToBoard extends StatefulWidget { + const EmailToBoard({super.key}); + + @override + State createState() => _EmailToBoardState(); +} + +class _EmailToBoardState extends State { + final TextEditingController emailController = TextEditingController(); + String? dropdownValue; + String? dropdownPosition; + List list = ["To Do"]; + List position = ["Bottom"]; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Email-to-board settings"), + centerTitle: false, + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Your email address for this board"), + TextField( + controller: emailController, + ), + ListTile( + leading: const Icon(Icons.copy), + title: const Text("Copy this address"), + onTap: () {}, + ), + ListTile( + leading: const Icon(Icons.email_outlined), + title: const Text("Email me this address"), + onTap: () {}, + ), + ListTile( + leading: const Icon(Icons.mark_email_read_outlined), + title: const Text("Generate a new email address"), + onTap: () {}, + ), + const Divider( + height: 2, + thickness: 1, + ), + const Padding( + padding: EdgeInsets.only(bottom: 8.0, top: 8.0), + child: Text( + "Your emailed cards appear in...", + style: TextStyle(fontWeight: FontWeight.w600), + ), + ), + const Text( + "List", + style: + TextStyle(color: brandColor, fontWeight: FontWeight.bold), + ), + DropdownButton( + isExpanded: true, + value: dropdownValue, + icon: const Icon(Icons.keyboard_arrow_down), + elevation: 16, + style: const TextStyle(color: themeColor), + underline: Container( + height: 2, + color: brandColor, + ), + onChanged: (String? value) { + setState(() { + dropdownValue = value!; + }); + }, + items: list.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + const Text( + "Position", + style: + TextStyle(color: brandColor, fontWeight: FontWeight.bold), + ), + DropdownButton( + isExpanded: true, + value: dropdownPosition, + icon: const Icon(Icons.keyboard_arrow_down), + elevation: 16, + style: const TextStyle(color: themeColor), + underline: Container( + height: 2, + color: brandColor, + ), + onChanged: (String? value) { + setState(() { + dropdownPosition = value!; + }); + }, + items: position.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + const Padding( + padding: EdgeInsets.only(top: 18.0), + child: Divider( + height: 2, + thickness: 1, + ), + ), + const Padding( + padding: EdgeInsets.only(top: 18.0), + child: Text( + "Tip: Don't share this email address. Anyone who has it can add cards as you. When composing emails , the card title goes in the subject and the card description in the body", + style: TextStyle(fontWeight: FontWeight.bold), + ), + ) + ], + ), + ), + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/emptywidget/index.dart b/demos/supabase-trello/lib/features/emptywidget/index.dart new file mode 100644 index 00000000..3bda2c34 --- /dev/null +++ b/demos/supabase-trello/lib/features/emptywidget/index.dart @@ -0,0 +1,389 @@ +// ignore_for_file: constant_identifier_names, camel_case_extensions + +import 'dart:math'; +import 'package:flutter/material.dart'; + +/// {@tool snippet} +/// +/// This example shows how to use [EmptyWidget] +/// +/// ``` dart +/// EmptyWidget( +/// image: null, +/// packageImage: PackageImage.Image_1, +/// title: 'No Notification', +/// subTitle: 'No notification available yet', +/// titleTextStyle: TextStyle( +/// fontSize: 22, +/// color: Color(0xff9da9c7), +/// fontWeight: FontWeight.w500, +/// ), +/// subtitleTextStyle: TextStyle( +/// fontSize: 14, +/// color: Color(0xffabb8d6), +/// ), +/// ) +/// ``` +/// {@end-tool} + +class EmptyWidget extends StatefulWidget { + const EmptyWidget({ + super.key, + this.title, + this.subTitle, + this.image, + this.subtitleTextStyle, + this.titleTextStyle, + this.packageImage, + this.hideBackgroundAnimation = false, + }); + + /// Display images from project assets + final String? image; /*!*/ + + /// Display image from package assets + final PackageImage? packageImage; /*!*/ + + /// Set text for subTitle + final String? subTitle; /*!*/ + + /// Set text style for subTitle + final TextStyle? subtitleTextStyle; /*!*/ + + /// Set text for title + final String? title; /*!*/ + + /// Text style for title + final TextStyle? titleTextStyle; /*!*/ + + /// Hides the background circular ball animation + /// + /// By default `false` value is set + final bool? hideBackgroundAnimation; + + @override + State createState() => _EmptyListWidgetState(); +} + +class _EmptyListWidgetState extends State + with TickerProviderStateMixin { + // String title, subTitle,image = 'assets/images/emptyImage.png'; + + late AnimationController _backgroundController; + + late Animation _imageAnimation; /*!*/ + AnimationController? _imageController; /*!*/ + late PackageImage? _packageImage; /*!*/ + TextStyle? _subtitleTextStyle; /*!*/ + TextStyle? _titleTextStyle; /*!*/ + late AnimationController _widgetController; /*!*/ + + @override + void dispose() { + _backgroundController.dispose(); + _imageController!.dispose(); + _widgetController.dispose(); + super.dispose(); + } + + @override + void initState() { + _backgroundController = AnimationController( + duration: const Duration(minutes: 1), + vsync: this, + lowerBound: 0, + upperBound: 20) + ..repeat(); + _widgetController = AnimationController( + duration: const Duration(seconds: 1), + vsync: this, + lowerBound: 0, + upperBound: 1) + ..forward(); + _imageController = AnimationController( + duration: const Duration(seconds: 4), + vsync: this, + )..repeat(); + _imageAnimation = Tween(begin: 0, end: 10).animate( + CurvedAnimation(parent: _imageController!, curve: Curves.linear), + ); + super.initState(); + } + + animationListner() { + if (_imageController == null) { + return; + } + if (_imageController!.isCompleted) { + setState(() { + _imageController!.reverse(); + }); + } else { + setState(() { + _imageController!.forward(); + }); + } + } + + Widget _imageWidget() { + bool isPackageImage = _packageImage != null; + return Expanded( + flex: 3, + child: AnimatedBuilder( + animation: _imageAnimation, + builder: (BuildContext context, Widget? child) { + return Transform.translate( + offset: Offset( + 0, + sin(_imageAnimation.value > .9 + ? 1 - _imageAnimation.value + : _imageAnimation.value)), + child: child, + ); + }, + child: Padding( + padding: const EdgeInsets.all(10), + child: Image.asset( + isPackageImage ? _packageImage.encode()! : widget.image!, + fit: BoxFit.contain, + package: isPackageImage ? 'empty_widget' : null, + ), + ), + ), + ); + } + + Widget _imageBackground() { + return Container( + width: EmptyWidgetUtility.getHeightDimention( + context, EmptyWidgetUtility.fullWidth(context) * .95), + height: EmptyWidgetUtility.getHeightDimention( + context, EmptyWidgetUtility.fullWidth(context) * .95), + decoration: const BoxDecoration(boxShadow: [ + BoxShadow( + offset: Offset(0, 0), + color: Color(0xffe2e5ed), + ), + BoxShadow( + blurRadius: 30, + offset: Offset(20, 0), + color: Color(0xffffffff), + spreadRadius: -5), + ], shape: BoxShape.circle), + ); + } + + Widget _shell({Widget? child}) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + if (constraints.maxHeight > constraints.maxWidth) { + return SizedBox( + height: constraints.maxWidth, + width: constraints.maxWidth, + child: child, + ); + } else { + return child!; + } + }); + } + + Widget _shellChild() { + _titleTextStyle = widget.titleTextStyle ?? + Theme.of(context) + .typography + .dense + .headlineSmall! + .copyWith(color: const Color(0xff9da9c7)); + _subtitleTextStyle = widget.subtitleTextStyle ?? + Theme.of(context) + .typography + .dense + .bodyMedium! + .copyWith(color: const Color(0xffabb8d6)); + _packageImage = widget.packageImage; + + bool anyImageProvided = widget.image == null && _packageImage == null; + + return FadeTransition( + opacity: _widgetController, + child: Container( + alignment: Alignment.center, + color: Colors.transparent, + child: Stack( + alignment: Alignment.center, + children: [ + if (!widget.hideBackgroundAnimation!) + RotationTransition( + turns: _backgroundController, + child: _imageBackground(), + ), + LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Container( + height: constraints.maxWidth, + width: constraints.maxWidth - 30, + alignment: Alignment.center, + padding: const EdgeInsets.all(10), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + anyImageProvided + ? const SizedBox() + : Expanded( + flex: 1, + child: Container(), + ), + anyImageProvided ? const SizedBox() : _imageWidget(), + Column( + children: [ + CustomText( + msg: widget.title, + style: _titleTextStyle, + context: context, + overflow: TextOverflow.clip, + textAlign: TextAlign.center, + ), + const SizedBox( + height: 10, + ), + CustomText( + msg: widget.subTitle, + style: _subtitleTextStyle, + context: context, + overflow: TextOverflow.clip, + textAlign: TextAlign.center) + ], + ), + anyImageProvided + ? const SizedBox() + : Expanded( + flex: 1, + child: Container(), + ) + ], + ), + ); + }), + ], + )), + ); + } + + @override + Widget build(BuildContext context) { + return _shell(child: _shellChild()); + } +} + +// nodoc +enum PackageImage { + Image_1, + Image_2, + Image_3, + Image_4, +} + +const _$PackageImageTypeMap = { + PackageImage.Image_1: 'assets/images/emptyImage.png', + PackageImage.Image_2: 'assets/images/im_emptyIcon_1.png', + PackageImage.Image_3: 'assets/images/im_emptyIcon_2.png', + PackageImage.Image_4: 'assets/images/im_emptyIcon_3.png', +}; + +extension convert on PackageImage? { + String? encode() => _$PackageImageTypeMap[this!]; + + PackageImage? key(String value) => decodePackageImage(value); + + PackageImage? decodePackageImage(String value) { + return _$PackageImageTypeMap.entries + .singleWhere((element) => element.value == value) + .key; + } +} + +class EmptyWidgetUtility { + static double getHeightDimention(BuildContext context, double unit) { + if (fullHeight(context) <= 460.0) { + return unit / 1.5; + } else { + return getDimention(context, unit); + } + } + + static double fullHeight(BuildContext context) { + return MediaQuery.of(context).size.height; + } + + static double getDimention(context, double unit) { + if (fullWidth(context) <= 360.0) { + return unit / 1.3; + } else { + return unit; + } + } + + static double fullWidth(BuildContext context) { + return MediaQuery.of(context).size.width; + } +} + +class CustomText extends StatefulWidget { + const CustomText( + {super.key, + this.msg, + this.style, + this.textAlign, + this.overflow, + this.context, + this.softwrap}); + + final BuildContext? context; + final String? msg; + final TextOverflow? overflow; + final bool? softwrap; + final TextStyle? style; + final TextAlign? textAlign; + + @override + // ignore: library_private_types_in_public_api + _CustomTextState createState() => _CustomTextState(); +} + +class _CustomTextState extends State { + TextStyle? style; + + @override + @override + void initState() { + style = widget.style; + super.initState(); + } + + Widget customText() { + if (widget.msg == null) { + return Container(); + } + if (widget.context != null && widget.style != null) { + var font = widget.style!.fontSize == null + ? Theme.of(context).textTheme.bodyMedium!.fontSize! + : widget.style!.fontSize!; + style = widget.style!.copyWith( + fontSize: + font - (EmptyWidgetUtility.fullWidth(context) <= 375 ? 2 : 0)); + } + return Text( + widget.msg!, + style: widget.style, + textAlign: widget.textAlign, + overflow: widget.overflow, + ); + } + + @override + Widget build(BuildContext context) { + return customText(); + } +} diff --git a/demos/supabase-trello/lib/features/generateworkspace/presentation/index.dart b/demos/supabase-trello/lib/features/generateworkspace/presentation/index.dart new file mode 100644 index 00000000..f45307a4 --- /dev/null +++ b/demos/supabase-trello/lib/features/generateworkspace/presentation/index.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/data_generator.dart'; + +import '../../../main.dart'; +import '../../../utils/service.dart'; + +class GenerateWorkspace extends StatefulWidget { + const GenerateWorkspace({super.key}); + + @override + State createState() => _GenerateWorkspaceState(); +} + +class _GenerateWorkspaceState extends State with Service { + final TextEditingController nameController = TextEditingController(); + Map? dropdownValue; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Generate Sample Workspace"), + centerTitle: false, + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.only(bottom: 8.0), + child: Text( + "This will create a Workspace with sample data", + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + ), + ), + TextField( + controller: nameController, + decoration: + const InputDecoration(hintText: "Enter workspace name"), + ), + // const Padding( + // padding: EdgeInsets.only(top: 8.0), + // child: Text("Visibility"), + // ), + // DropdownButton>( + // isExpanded: true, + // value: dropdownValue, + // icon: const Icon(Icons.keyboard_arrow_down), + // elevation: 16, + // style: const TextStyle(color: brandColor), + // underline: Container( + // height: 2, + // color: brandColor, + // ), + // onChanged: (Map? value) { + // // This is called when the user selects an item. + // setState(() { + // dropdownValue = value!; + // }); + // }, + // items: visibilityConfigurations + // .map>>( + // (Map value) { + // return DropdownMenuItem>( + // value: value, + // child: Text(value["type"]!), + // ); + // }).toList(), + // ), + // const Padding( + // padding: EdgeInsets.only(top: 10.0), + // child: Text("Description"), + // ), + // TextField( + // controller: descriptionController, + // maxLines: null, + // minLines: 4, + // ), + Align( + alignment: Alignment.center, + child: Container( + padding: const EdgeInsets.only(top: 10), + width: MediaQuery.of(context).size.width * 0.8, + height: 60, + child: ElevatedButton( + onPressed: () { + DataGenerator().createSampleWorkspace( + nameController.text, trello, context); + }, + child: const Text("Create"))), + ) + ], + ), + ), + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/home/presentation/custom_floating_action.dart b/demos/supabase-trello/lib/features/home/presentation/custom_floating_action.dart new file mode 100644 index 00000000..e9532adf --- /dev/null +++ b/demos/supabase-trello/lib/features/home/presentation/custom_floating_action.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +class CustomFloatingAction extends StatefulWidget { + final String title; + final IconData icon; + final String route; + const CustomFloatingAction(this.title, this.icon, this.route, {super.key}); + + @override + State createState() => _CustomFloatingActionState(); +} + +class _CustomFloatingActionState extends State { + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + Navigator.pushNamed(context, widget.route); + }, + child: Text.rich(TextSpan(children: [ + WidgetSpan( + child: SizedBox( + width: 150, + height: 30, + child: Card( + child: Center( + child: Text(widget.title), + )), + )), + const WidgetSpan( + child: SizedBox( + width: 20, + )), + WidgetSpan( + child: CircleAvatar( + backgroundColor: Colors.green[400], + child: Icon(widget.icon, color: Colors.white, size: 26), + )) + ])), + ); + } +} diff --git a/demos/supabase-trello/lib/features/home/presentation/custom_search.dart b/demos/supabase-trello/lib/features/home/presentation/custom_search.dart new file mode 100644 index 00000000..ed0394cc --- /dev/null +++ b/demos/supabase-trello/lib/features/home/presentation/custom_search.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/models/board.dart'; + +class CustomSearchDelegate extends SearchDelegate { + List searchTerms = []; + CustomSearchDelegate(List s) { + searchTerms = s; + } + + @override + List? buildActions(BuildContext context) { + return [ + IconButton( + onPressed: () { + query = ''; + }, + icon: const Icon(Icons.clear), + ), + ]; + } + + @override + Widget? buildLeading(BuildContext context) { + return IconButton( + onPressed: () { + close(context, null); + }, + icon: const Icon(Icons.arrow_back), + ); + } + + @override + Widget buildResults(BuildContext context) { + List matchQuery = []; + for (var brd in searchTerms) { + if (brd.name.toLowerCase().contains(query.toLowerCase())) { + matchQuery.add(brd); + } + } + return ListView.builder( + itemCount: matchQuery.length, + itemBuilder: (context, index) { + var result = matchQuery[index]; + + return ListTile( + onTap: () async { + if (context.mounted) { + Navigator.pushNamed(context, "/board"); + } + }, + title: Text(result.name), + ); + }, + ); + } + + @override + Widget buildSuggestions(BuildContext context) { + List matchQuery = []; + for (var brd in searchTerms) { + if (brd.name.toLowerCase().contains(query.toLowerCase())) { + matchQuery.add(brd); + } + } + return ListView.builder( + itemCount: matchQuery.length, + itemBuilder: (context, index) { + var result = matchQuery[index]; + + return ListTile( + onTap: () async { + if (context.mounted) { + Navigator.pushNamed(context, "/board"); + } + }, + title: Text(result.name), + ); + }, + ); + } +} diff --git a/demos/supabase-trello/lib/features/home/presentation/index.dart b/demos/supabase-trello/lib/features/home/presentation/index.dart new file mode 100644 index 00000000..3cb27211 --- /dev/null +++ b/demos/supabase-trello/lib/features/home/presentation/index.dart @@ -0,0 +1,183 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_expandable_fab/flutter_expandable_fab.dart'; +import 'package:logging/logging.dart'; +import 'package:powersync/powersync.dart' as power_sync; +import 'package:trelloappclone_flutter/features/emptywidget/index.dart'; +import 'package:trelloappclone_flutter/main.dart'; +import 'package:trelloappclone_flutter/features/board/domain/board_arguments.dart'; +import 'package:trelloappclone_flutter/features/board/presentation/index.dart'; +import 'package:trelloappclone_flutter/protocol/data_client.dart'; + +import '../../../utils/color.dart'; +import '../../../utils/service.dart'; +import '../../../utils/widgets.dart'; +import '../../drawer/presentation/index.dart'; +import 'custom_floating_action.dart'; + +final log = Logger('powersync-supabase'); + +class Home extends StatefulWidget { + const Home({super.key}); + + @override + State createState() => _HomeState(); +} + +class _HomeState extends State with Service { + late power_sync.SyncStatus _connectionState; + StreamSubscription? _syncStatusSubscription; + + @override + void initState() { + super.initState(); + + _connectionState = dataClient.getCurrentSyncStatus(); + _syncStatusSubscription = dataClient.getStatusStream().listen((event) { + setState(() { + _connectionState = event; + }); + }); + } + + @override + void dispose() { + super.dispose(); + _syncStatusSubscription?.cancel(); + } + + @override + Widget build(BuildContext context) { + IconButton connectedIcon = IconButton( + icon: const Icon(Icons.wifi), + tooltip: 'Connected', + onPressed: () { + switchToOfflineMode(); + }, + ); + IconButton disconnectedIcon = IconButton( + icon: const Icon(Icons.wifi_off), + tooltip: 'Not connected', + onPressed: () { + switchToOnlineMode(); + }, + ); + + return Scaffold( + appBar: AppBar( + title: const Text("Boards"), + actions: [ + IconButton( + onPressed: () { + search(context); + }, + icon: const Icon(Icons.search)), + _connectionState.connected ? connectedIcon : disconnectedIcon + ], + ), + drawer: const CustomDrawer(), + body: StreamBuilder( + stream: getWorkspacesStream(), + builder: + (BuildContext context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + List children = snapshot.data as List; + + if (children.isNotEmpty) { + return SingleChildScrollView( + child: + Column(children: buildWorkspacesAndBoards(children))); + } + } + return const Center( + child: Padding( + padding: EdgeInsets.all(20.0), + child: EmptyWidget( + image: null, + title: 'No Boards', + subTitle: 'Create your first Trello board', + titleTextStyle: TextStyle( + fontSize: 22, + color: Color(0xff9da9c7), + fontWeight: FontWeight.w500, + ), + subtitleTextStyle: TextStyle( + fontSize: 14, + color: Color(0xffabb8d6), + ), + ), + ), + ); + }), + floatingActionButtonLocation: ExpandableFab.location, + floatingActionButton: ExpandableFab( + openButtonBuilder: RotateFloatingActionButtonBuilder( + child: const Icon(Icons.add), + fabSize: ExpandableFabSize.regular, + backgroundColor: Colors.green[400], + shape: const CircleBorder(), + ), + type: ExpandableFabType.up, + children: const [ + CustomFloatingAction("Workspace", Icons.book, '/createworkspace'), + CustomFloatingAction("Board", Icons.book, '/createboard'), + CustomFloatingAction("Sample Workspace", Icons.dataset_outlined, + '/generateworkspace'), + //CustomFloatingAction("Card", Icons.card_membership, '/createcard') + ]), + ); + } + + List buildWorkspacesAndBoards(List wkspcs) { + List workspacesandboards = []; + Widget workspace; + + for (int i = 0; i < wkspcs.length; i++) { + workspace = ListTile( + tileColor: whiteShade, + leading: Text(wkspcs[i].name), + trailing: IconButton( + onPressed: () { + Navigator.pushNamed(context, '/workspacemenu'); + }, + icon: const Icon(Icons.more_horiz)), + ); + + workspacesandboards.add(workspace); + + workspacesandboards.add(StreamBuilder( + stream: getBoardsStream(wkspcs[i].id), + builder: (BuildContext context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + List children = snapshot.data as List; + + if (children.isNotEmpty) { + return Column(children: buildBoards(children, wkspcs[i])); + } + } + return const SizedBox.shrink(); + }, + )); + } + return workspacesandboards; + } + + List buildBoards(List brd, Workspace wkspc) { + List boards = []; + for (int j = 0; j < brd.length; j++) { + boards.add(ListTile( + leading: ColorSquare( + bckgrd: brd[j].background, + ), + title: Text(brd[j].name), + onTap: () { + Navigator.pushNamed(context, BoardScreen.routeName, + arguments: BoardArguments(brd[j], wkspc)); + }, + )); + } + + return boards; + } +} diff --git a/demos/supabase-trello/lib/features/invitemember/presentation/index.dart b/demos/supabase-trello/lib/features/invitemember/presentation/index.dart new file mode 100644 index 00000000..cb63d80f --- /dev/null +++ b/demos/supabase-trello/lib/features/invitemember/presentation/index.dart @@ -0,0 +1,187 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; +import 'package:trelloappclone_flutter/utils/service.dart'; +import 'package:trelloappclone_flutter/models/member.dart'; + +import '../../../main.dart'; + +class InviteMember extends StatefulWidget { + const InviteMember({super.key}); + + @override + State createState() => _InviteMemberState(); +} + +class _InviteMemberState extends State with Service { + final TextEditingController emailcontroller = TextEditingController(); + final List _currentMembers = []; + + @override + void initState() { + super.initState(); + _currentMembers.addAll(trello.selectedWorkspace.members ?? []); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: const Icon(Icons.close, size: 30), + ), + title: Text("Invite to ${trello.selectedWorkspace.name}"), + centerTitle: false, + // actions: [ + // IconButton(onPressed: () {}, icon: const Icon(Icons.contacts)) + // ], + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: TextField( + controller: emailcontroller, + textCapitalization: TextCapitalization.none, + keyboardType: TextInputType.emailAddress, + decoration: const InputDecoration(hintText: "Email"), + ), + ), + Card( + child: ListTile( + textColor: brandColor, + title: const Text("Add Existing User"), + subtitle: const Text("Add user with email to workspace."), + trailing: IconButton( + icon: const Icon( + Icons.add_circle_outline, + color: brandColor, + ), + onPressed: () { + inviteUserToWorkspace( + emailcontroller.text, trello.selectedWorkspace) + .then((succeeded) { + if (succeeded) { + setState(() { + _currentMembers.clear(); + _currentMembers + .addAll(trello.selectedWorkspace.members ?? []); + }); + // ignore: use_build_context_synchronously + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 3), + margin: EdgeInsets.only( + // ignore: use_build_context_synchronously + bottom: MediaQuery.of(context).size.height * + 0.1, // 10% from bottom + right: 20, + left: 20, + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Row( + children: [ + Icon(Icons.check, color: brandColor), + SizedBox(width: 12), + Text('Added Member'), + ], + ), + const SizedBox(height: 4), + Text( + '${emailcontroller.text} added to workspace.'), + ], + ), + behavior: SnackBarBehavior.floating, + ), + ); + } else { + // ignore: use_build_context_synchronously + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 3), + margin: EdgeInsets.only( + // ignore: use_build_context_synchronously + bottom: MediaQuery.of(context).size.height * + 0.1, // 10% from bottom + right: 20, + left: 20, + ), + backgroundColor: Colors.red[100], + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Row( + children: [ + Icon(Icons.error_outline, + color: Colors.red), + SizedBox(width: 12), + Text('Add Failed'), + ], + ), + const SizedBox(height: 4), + Text( + '${emailcontroller.text} not an existing user.'), + ], + ), + behavior: SnackBarBehavior.floating, + ), + ); + } + }); + }, + ), + ), + ), + const Padding( + padding: EdgeInsets.only(top: 18.0, bottom: 18), + child: Align( + alignment: Alignment.topLeft, + child: Text( + "Current Board Members", + style: TextStyle(fontWeight: FontWeight.w600), + ), + ), + ), + _buildMembersList(), + // Padding( + // padding: EdgeInsets.only(bottom: 8.0), + // child: Text( + // "Work together on a board", + // style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + // ), + // ), + // Text( + // "Use the search bar or invite link to share this board with others", + // textAlign: TextAlign.center, + // ) + ], + ), + ), + ), + ); + } + + Widget _buildMembersList() { + List memberTiles = []; + for (var member in _currentMembers) { + memberTiles.add(ListTile( + leading: CircleAvatar( + backgroundColor: brandColor, + child: Text(member.name[0].toUpperCase()), + ), + title: Text(member.name), + trailing: const Text("Admin"), + )); + } + return Column( + children: memberTiles, + ); + } +} diff --git a/demos/supabase-trello/lib/features/landing/presentation/bottomsheet.dart b/demos/supabase-trello/lib/features/landing/presentation/bottomsheet.dart new file mode 100644 index 00000000..cd217feb --- /dev/null +++ b/demos/supabase-trello/lib/features/landing/presentation/bottomsheet.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:trelloappclone_flutter/features/signtotrello/domain/sign_arguments.dart'; +import 'package:trelloappclone_flutter/features/signtotrello/presentation/index.dart'; + +import '../../../utils/color.dart'; +import '../../../utils/config.dart'; + +class LandingBottomSheet extends StatefulWidget { + final Enum type; + const LandingBottomSheet(this.type, {super.key}); + + @override + State createState() => _LandingBottomSheetState(); +} + +class _LandingBottomSheetState extends State { + @override + Widget build(BuildContext context) { + return SizedBox( + height: 250, + child: ListView( + children: [ + ListTile( + onTap: () { + Navigator.pop(context); + Navigator.pushNamed(context, SignToTrello.routeName, + arguments: SignArguments(widget.type)); + }, + leading: const Icon( + Icons.email, + color: brandColor, + ), + title: Text( + (widget.type == Sign.signUp) + ? " SIGN UP WITH EMAIL" + : "LOG IN WITH EMAIL", + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + ListTile( + onTap: null, + leading: Icon( + MdiIcons.google, + color: Colors.grey, + ), + title: Text( + (widget.type == Sign.signUp) + ? " SIGN UP WITH GOOGLE" + : "LOG IN WITH GOOGLE", + style: const TextStyle( + fontWeight: FontWeight.bold, color: Colors.grey), + ), + ), + ListTile( + onTap: null, + leading: Icon( + MdiIcons.microsoft, + color: Colors.grey, + ), + title: Text( + (widget.type == Sign.signUp) + ? " SIGN UP WITH MICROSOFT" + : "LOG IN WITH MICROSOFT", + style: const TextStyle( + fontWeight: FontWeight.bold, color: Colors.grey), + ), + ), + ListTile( + onTap: null, + leading: Icon( + MdiIcons.apple, + color: Colors.grey, + ), + title: Text( + (widget.type == Sign.signUp) + ? " SIGN UP WITH APPLE" + : "LOG IN WITH APPLE", + style: const TextStyle( + fontWeight: FontWeight.bold, color: Colors.grey), + ), + ) + ], + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/landing/presentation/index.dart b/demos/supabase-trello/lib/features/landing/presentation/index.dart new file mode 100644 index 00000000..6a5b6e6c --- /dev/null +++ b/demos/supabase-trello/lib/features/landing/presentation/index.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/features/landing/presentation/bottomsheet.dart'; + +import '../../../utils/color.dart'; +import '../../../utils/config.dart'; +import '../../../utils/constant.dart'; +import '../../../utils/service.dart'; + +class Landing extends StatefulWidget { + const Landing({super.key}); + + @override + State createState() => _LandingState(); +} + +class _LandingState extends State with Service { + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Image.asset( + logo, + width: 30, + height: 30, + ), + ), + Image.asset( + landingImage, + height: MediaQuery.of(context).size.height * 0.4, + ), + const Padding( + padding: EdgeInsets.all(25.0), + child: Text( + headline, + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + ), + Container( + margin: const EdgeInsets.all(8.0), + width: MediaQuery.of(context).size.width * 0.8, + height: 50, + child: ElevatedButton( + onPressed: () { + showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return const LandingBottomSheet(Sign.signUp); + }); + }, + child: const Text("Sign up"), + ), + ), + Container( + margin: const EdgeInsets.all(8.0), + width: MediaQuery.of(context).size.width * 0.8, + height: 50, + child: OutlinedButton( + onPressed: () { + showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return const LandingBottomSheet(Sign.logIn); + }); + }, + style: OutlinedButton.styleFrom( + side: const BorderSide(width: 1.0, color: brandColor)), + child: const Text("Log in"), + ), + ), + const Text( + terms, + textAlign: TextAlign.center, + ), + const SizedBox( + height: 10, + ), + const Text( + contact, + style: TextStyle(decoration: TextDecoration.underline), + ) + ], + )), + ); + } +} diff --git a/demos/supabase-trello/lib/features/members/presentation/index.dart b/demos/supabase-trello/lib/features/members/presentation/index.dart new file mode 100644 index 00000000..c6ae50f1 --- /dev/null +++ b/demos/supabase-trello/lib/features/members/presentation/index.dart @@ -0,0 +1,131 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; +import 'package:trelloappclone_flutter/utils/service.dart'; +import 'package:trelloappclone_flutter/models/member.dart'; + +import '../../../main.dart'; + +class Members extends StatefulWidget { + const Members({super.key}); + + @override + State createState() => _MembersState(); +} + +class _MembersState extends State with Service { + final List _currentMembers = []; + + @override + void initState() { + super.initState(); + _currentMembers.addAll(trello.selectedWorkspace.members ?? []); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Members"), + centerTitle: false, + actions: [ + TextButton( + onPressed: () { + Navigator.pushNamed(context, '/invitemember'); + }, + child: const Text( + "INVITE", + style: TextStyle(color: whiteShade), + )) + ], + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Members (${_currentMembers.length})"), + ListView( + shrinkWrap: true, + children: _buildMembersList(), + ) + ], + ), + )), + ); + } + + List _buildMembersList() { + List memberTiles = []; + for (var member in _currentMembers) { + memberTiles.add(ListTile( + leading: CircleAvatar( + backgroundColor: brandColor, + child: Text(member.name[0].toUpperCase()), + ), + title: Text(member.name), + trailing: Text( + member.role, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + onTap: () { + showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return SizedBox( + height: MediaQuery.of(context).size.height * 0.4, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile( + leading: CircleAvatar( + backgroundColor: brandColor, + child: Text(member.name[0].toUpperCase()), + ), + title: Text(member.name), + ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(member.role), + ), + const Text( + "Can view, create and edit Workspace boards, and change settings for the workspace"), + Align( + alignment: Alignment.center, + child: Container( + padding: const EdgeInsets.only(top: 8.0), + width: MediaQuery.of(context).size.width * 0.8, + height: 50, + child: ElevatedButton( + onPressed: () { + removeMemberFromWorkspace( + member, trello.selectedWorkspace) + .then((updatedWorkspace) { + setState(() { + _currentMembers.clear(); + _currentMembers.addAll( + updatedWorkspace.members ?? []); + }); + }); + Navigator.of(context).pop(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: dangerColor), + child: Text(member.userId == trello.user.id + ? "Leave workspace" + : "Remove from workspace")), + ), + ) + ]), + ), + ); + }); + }, + )); + } + + return memberTiles; + } +} diff --git a/demos/supabase-trello/lib/features/mycards/presentation/index.dart b/demos/supabase-trello/lib/features/mycards/presentation/index.dart new file mode 100644 index 00000000..25416bdc --- /dev/null +++ b/demos/supabase-trello/lib/features/mycards/presentation/index.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; + +import '../../drawer/presentation/index.dart'; + +class MyCards extends StatefulWidget { + const MyCards({super.key}); + + @override + State createState() => _MyCardsState(); +} + +class _MyCardsState extends State { + String selectedValue = "Board"; + List list = ["Board", "Date"]; + String? dropdownValue; + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Row(children: [ + Text("My cards by $selectedValue"), + SizedBox( + child: DropdownButton( + value: dropdownValue, + icon: const Icon( + Icons.keyboard_arrow_down, + color: Colors.white, + ), + underline: const SizedBox.shrink(), + elevation: 16, + onChanged: (String? value) { + setState(() { + selectedValue = value!; + }); + }, + items: list.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + )) + ]), + centerTitle: false, + actions: [IconButton(onPressed: () {}, icon: const Icon(Icons.search))], + ), + drawer: const CustomDrawer(), + body: const Center( + child: Padding( + padding: EdgeInsets.all(10.0), + child: Text( + "When you are assigned to cards they will show up here", + textAlign: TextAlign.center, + style: TextStyle(fontSize: 16), + ), + ), + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/notifications/presentation/index.dart b/demos/supabase-trello/lib/features/notifications/presentation/index.dart new file mode 100644 index 00000000..a814adea --- /dev/null +++ b/demos/supabase-trello/lib/features/notifications/presentation/index.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; + +class Notifications extends StatefulWidget { + const Notifications({super.key}); + + @override + State createState() => _NotificationsState(); +} + +class _NotificationsState extends State { + List popupmenu = ["Push notification settings"]; + late String selectedMenu; + List list = ["All Types", "Me", "Comments", "Join requests"]; + String selected = "All Types"; + bool show = true; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: const Icon( + Icons.close, + size: 30, + )), + actions: [ + IconButton( + onPressed: () {}, + icon: const Icon( + Icons.library_add_check_sharp, + size: 30, + )), + PopupMenuButton( + initialValue: popupmenu[0], + onSelected: (String item) { + setState(() { + selectedMenu = item; + }); + }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: popupmenu[0], + child: Text(popupmenu[0]), + ) + ]) + ], + ), + body: Stack(children: [ + Row( + children: [ + OutlinedButton( + onPressed: () { + showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return SizedBox( + height: 250, + child: ListView(children: buildWidgets()), + ); + }); + }, + child: Row( + children: [ + Text(selected), + const Icon(Icons.keyboard_arrow_down) + ], + )), + Visibility( + visible: show, + child: OutlinedButton( + onPressed: () { + setState(() { + show = !show; + }); + }, + child: const Text("Unread"), + )), + Visibility( + visible: !show, + child: ElevatedButton.icon( + onPressed: () { + setState(() { + show = !show; + }); + }, + icon: const Icon(Icons.check), + label: const Text("Unread"))), + ], + ), + const Center( + child: Padding( + padding: EdgeInsets.all(10.0), + child: Text( + "You don't have any notifications that match the selected filters", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), + textAlign: TextAlign.center, + ), + ), + ) + ]), + ); + } + + List buildWidgets() { + List dropdownLists = []; + for (int i = 0; i < list.length; i++) { + dropdownLists.add(ListTile( + onTap: () { + setState(() { + selected = list[i]; + }); + Navigator.pop(context); + }, + leading: + (selected == list[i]) ? const Icon(Icons.check) : const SizedBox(), + title: Text( + list[i], + style: const TextStyle(fontWeight: FontWeight.bold), + ), + )); + } + return dropdownLists; + } +} diff --git a/demos/supabase-trello/lib/features/offlineboards/presentation/index.dart b/demos/supabase-trello/lib/features/offlineboards/presentation/index.dart new file mode 100644 index 00000000..ae59e8e7 --- /dev/null +++ b/demos/supabase-trello/lib/features/offlineboards/presentation/index.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/models/board.dart'; +import 'package:trelloappclone_flutter/models/workspace.dart'; +import 'package:trelloappclone_flutter/features/drawer/presentation/index.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; + +import '../../../utils/service.dart'; +import '../../../utils/widgets.dart'; + +class OfflineBoards extends StatefulWidget { + const OfflineBoards({super.key}); + + @override + State createState() => _OfflineBoardsState(); +} + +class _OfflineBoardsState extends State with Service { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Offline boards"), + actions: [IconButton(onPressed: () {}, icon: const Icon(Icons.search))], + ), + drawer: const CustomDrawer(), + body: SingleChildScrollView( + child: StreamBuilder( + stream: getWorkspacesStream(), + builder: (BuildContext context, + AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + List children = snapshot.data as List; + + if (children.isNotEmpty) { + return Column(children: buildWorkspacesAndBoards(children)); + } + } + return const SizedBox.shrink(); + })), + ); + } + + List buildWorkspacesAndBoards(List wkspcs) { + List workspacesboards = []; + Widget workspace; + + for (int i = 0; i < wkspcs.length; i++) { + workspace = ListTile( + tileColor: whiteShade, + leading: Text(wkspcs[i].name), + ); + + workspacesboards.add(workspace); + + workspacesboards.add(StreamBuilder( + stream: getBoardsStream(wkspcs[i].id), + builder: (BuildContext context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + List children = snapshot.data as List; + + if (children.isNotEmpty) { + return Column(children: buildBoards(children, wkspcs[i])); + } + } + return const SizedBox.shrink(); + })); + } + // } + return workspacesboards; + } + + List buildBoards(List brd, Workspace wkspcs) { + List boards = []; + for (int j = 0; j < brd.length; j++) { + boards.add(ListTile( + leading: ColorSquare(bckgrd: brd[j].background), + title: Text(brd[j].name), + onTap: () {}, + trailing: Switch( + value: brd[j].availableOffline ?? false, + activeThumbColor: brandColor, + onChanged: (bool value) { + setState(() { + brd[j].availableOffline = value; + updateOfflineStatus(brd[j]); + }); + }, + ), + )); + } + return boards; + } +} diff --git a/demos/supabase-trello/lib/features/powerups/presentation/index.dart b/demos/supabase-trello/lib/features/powerups/presentation/index.dart new file mode 100644 index 00000000..a657bc34 --- /dev/null +++ b/demos/supabase-trello/lib/features/powerups/presentation/index.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +import '../../../utils/constant.dart'; + +class PowerUps extends StatefulWidget { + const PowerUps({super.key}); + + @override + State createState() => _PowerUpsState(); +} + +class _PowerUpsState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Power-Ups"), + centerTitle: false, + ), + body: ListView.separated( + itemBuilder: (BuildContext context, index) { + return ListTile( + leading: const CircleAvatar(), + title: Text(powerups[index]["title"]!), + subtitle: Text(powerups[index]["description"]!), + trailing: Switch( + value: false, + onChanged: ((value) {}), + ), + ); + }, + separatorBuilder: (context, index) => const Divider( + color: Colors.black, + ), + itemCount: powerups.length), + ); + } +} diff --git a/demos/supabase-trello/lib/features/settings/presentation/index.dart b/demos/supabase-trello/lib/features/settings/presentation/index.dart new file mode 100644 index 00000000..aa360798 --- /dev/null +++ b/demos/supabase-trello/lib/features/settings/presentation/index.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/features/drawer/presentation/index.dart'; + +class Settings extends StatefulWidget { + const Settings({super.key}); + + @override + State createState() => _SettingsState(); +} + +class _SettingsState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Settings"), + ), + drawer: const CustomDrawer(), + body: SingleChildScrollView( + child: Column( + children: [ + const ListTile( + subtitle: Text("Notifications"), + ), + const ListTile( + title: Text("Open system settings"), + ), + const Divider( + height: 2, + thickness: 2, + ), + const ListTile( + subtitle: Text("Application theme"), + ), + const ListTile( + title: Text("Select theme"), + ), + const Divider( + height: 2, + thickness: 2, + ), + const ListTile( + subtitle: Text("Accessibility"), + ), + ListTile( + title: const Text("Color blind friendly mode"), + trailing: Checkbox(value: false, onChanged: ((value) {})), + ), + ListTile( + title: const Text("Enable animations"), + trailing: Checkbox(value: true, onChanged: ((value) {})), + ), + ListTile( + title: const Text("Show label names on card front"), + trailing: Checkbox(value: false, onChanged: ((value) {})), + ), + const ListTile( + subtitle: Text("Sync"), + ), + const ListTile( + title: Text("Sync queue"), + ), + const ListTile( + subtitle: Text("General"), + ), + const ListTile( + title: Text("Profile and visibility"), + ), + const ListTile( + title: Text("Create card details"), + ), + const ListTile( + title: Text("Set app language"), + ), + ListTile( + title: const Text("Delete account"), + onTap: () { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text( + "Delete account?", + style: TextStyle(fontWeight: FontWeight.bold), + ), + content: const Text( + "You must log in on the web to delete your account"), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text("CANCEL")), + TextButton( + onPressed: () {}, child: const Text("GO TO WEB")) + ], + ); + }); + }, + ), + const ListTile( + title: Text("About Trello"), + ), + const ListTile( + title: Text("More Atlassian apps"), + ), + const ListTile( + title: Text("Contact support"), + ), + Padding( + padding: const EdgeInsets.only(bottom: 30.0), + child: ListTile( + title: const Text("Log out"), + onTap: () { + Navigator.pushNamed(context, '/'); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/signtotrello/domain/sign_arguments.dart b/demos/supabase-trello/lib/features/signtotrello/domain/sign_arguments.dart new file mode 100644 index 00000000..e6fceb51 --- /dev/null +++ b/demos/supabase-trello/lib/features/signtotrello/domain/sign_arguments.dart @@ -0,0 +1,5 @@ +class SignArguments { + final Enum type; + + SignArguments(this.type); +} diff --git a/demos/supabase-trello/lib/features/signtotrello/presentation/index.dart b/demos/supabase-trello/lib/features/signtotrello/presentation/index.dart new file mode 100644 index 00000000..7dc3f026 --- /dev/null +++ b/demos/supabase-trello/lib/features/signtotrello/presentation/index.dart @@ -0,0 +1,191 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/features/signtotrello/domain/sign_arguments.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; + +import '../../../utils/config.dart'; +import '../../../utils/service.dart'; + +class SignToTrello extends StatefulWidget { + const SignToTrello({super.key}); + + @override + State createState() => _SignToTrelloState(); + + static const routeName = '/sign'; +} + +class _SignToTrelloState extends State with Service { + final TextEditingController emailcontroller = TextEditingController(); + final TextEditingController usernamecontroller = TextEditingController(); + final TextEditingController passwordcontroller = TextEditingController(); + final TextEditingController confirmcontroller = TextEditingController(); + + @override + Widget build(BuildContext context) { + final args = ModalRoute.of(context)!.settings.arguments as SignArguments; + + return Scaffold( + appBar: AppBar( + title: Text( + (args.type == Sign.signUp) ? "Sign up" : " Log in to continue"), + centerTitle: false, + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only(top: 10.0), + child: Column( + children: [ + Image.asset( + logo, + width: 30, + height: 30, + ), + Padding( + padding: const EdgeInsets.only(bottom: 10.0, top: 10.0), + child: Text( + (args.type == Sign.signUp) + ? "Sign up to continue" + : "Log in to continue", + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + Padding( + padding: + const EdgeInsets.only(left: 20.0, right: 20.0, top: 10.0), + child: TextField( + controller: emailcontroller, + textCapitalization: TextCapitalization.none, + keyboardType: TextInputType.emailAddress, + decoration: + const InputDecoration(hintText: "Enter your email"), + ), + ), + (args.type == Sign.signUp) + ? Padding( + padding: const EdgeInsets.only( + left: 20.0, right: 20.0, top: 10.0), + child: TextField( + controller: usernamecontroller, + keyboardType: TextInputType.name, + decoration: + const InputDecoration(hintText: "Enter your name"), + ), + ) + : const SizedBox.shrink(), + Padding( + padding: const EdgeInsets.only( + left: 20.0, right: 20.0, top: 10.0, bottom: 10.0), + child: TextField( + controller: passwordcontroller, + obscureText: true, + decoration: + const InputDecoration(hintText: "Enter your password"), + ), + ), + (args.type == Sign.signUp) + ? Padding( + padding: const EdgeInsets.only( + left: 20.0, right: 20.0, top: 10.0), + child: TextField( + controller: confirmcontroller, + obscureText: true, + decoration: const InputDecoration( + hintText: "Confirm your password"), + )) + : const SizedBox.shrink(), + (args.type == Sign.signUp) + ? const Padding( + padding: EdgeInsets.all(20.0), + child: Text( + "By signing up, I accept the Atlassian Cloud Terms of Service and acknowledge the Privacy Policy"), + ) + : const SizedBox.shrink(), + SizedBox( + width: MediaQuery.of(context).size.width * 0.8, + height: 50, + child: ElevatedButton( + onPressed: () { + if (args.type == Sign.signUp && validateSignUp()) { + signUp( + name: usernamecontroller.text, + email: emailcontroller.text, + password: passwordcontroller.text, + context: context); + } else if (args.type == Sign.logIn && validateLogin()) { + logIn(emailcontroller.text, passwordcontroller.text, + context); + } + }, + style: + ElevatedButton.styleFrom(backgroundColor: brandColor), + child: Text( + (args.type == Sign.signUp) ? "Sign up" : "Log in")), + ), + // ListTile( + // onTap: () {}, + // leading: Icon( + // MdiIcons.google, + // color: brandColor, + // ), + // title: const Text( + // "CONTINUE WITH GOOGLE", + // style: TextStyle(fontWeight: FontWeight.bold), + // ), + // ), + // ListTile( + // onTap: () {}, + // leading: Icon( + // MdiIcons.microsoft, + // color: brandColor, + // ), + // title: const Text( + // "CONTINUE WITH MICROSOFT", + // style: TextStyle(fontWeight: FontWeight.bold), + // ), + // ), + // ListTile( + // onTap: () {}, + // leading: Icon( + // MdiIcons.apple, + // color: brandColor, + // ), + // title: const Text( + // "CONTINUE WITH APPLE", + // style: TextStyle(fontWeight: FontWeight.bold), + // ), + // ), + // GestureDetector( + // onTap: () {}, + // child: Text( + // (args.type == Sign.signUp) + // ? "Already have an Atlassian account? Log in" + // : "Can't log in? Create an account", + // style: const TextStyle( + // decoration: TextDecoration.underline, color: brandColor), + // ), + // ) + ], + ), + ), + ), + ); + } + + bool validateSignUp() { + if (emailcontroller.text.isNotEmpty && + emailcontroller.text.isNotEmpty && + passwordcontroller.text.isNotEmpty && + confirmcontroller.text.isNotEmpty && + confirmcontroller.text == passwordcontroller.text) { + return true; + } + return false; + } + + bool validateLogin() { + if (emailcontroller.text.isNotEmpty && passwordcontroller.text.isNotEmpty) { + return true; + } + return false; + } +} diff --git a/demos/supabase-trello/lib/features/viewmembers/presentation/index.dart b/demos/supabase-trello/lib/features/viewmembers/presentation/index.dart new file mode 100644 index 00000000..d35a8b9a --- /dev/null +++ b/demos/supabase-trello/lib/features/viewmembers/presentation/index.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +class ViewMembers extends StatefulWidget { + const ViewMembers({super.key}); + + @override + State createState() => _ViewMembersState(); +} + +class _ViewMembersState extends State { + List cardMembers = [ + {"name": "Jane Doe", "handle": "@janedoe"} + ]; + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text("Card Members"), + content: SizedBox( + height: 80, + child: Column(children: buildWidgets()), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text("DONE")) + ], + ); + } + + List buildWidgets() { + List members = []; + for (int i = 0; i < cardMembers.length; i++) { + members.add(ListTile( + leading: const CircleAvatar(), + title: Text(cardMembers[i]["name"]), + subtitle: Text(cardMembers[i]["handle"]), + )); + } + return members; + } +} diff --git a/demos/supabase-trello/lib/features/visibility/presentation/index.dart b/demos/supabase-trello/lib/features/visibility/presentation/index.dart new file mode 100644 index 00000000..a77394bd --- /dev/null +++ b/demos/supabase-trello/lib/features/visibility/presentation/index.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +import '../../../utils/constant.dart'; + +class BoardVisibility extends StatefulWidget { + const BoardVisibility({super.key}); + + @override + State createState() => _BoardVisibilityState(); +} + +class _BoardVisibilityState extends State { + List checked = [false, false, false]; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text("Visibility"), + content: SizedBox( + height: 360, + child: Column( + children: [ + Card( + child: ListTile( + leading: Checkbox( + value: checked[0], + onChanged: (bool? value) {}, + ), + title: Text(visibilityConfigurations[0]["type"]!), + subtitle: Text(visibilityConfigurations[0]["description"]!), + ), + ), + Card( + child: ListTile( + leading: Checkbox( + value: checked[1], + onChanged: (bool? value) {}, + ), + title: Text(visibilityConfigurations[1]["type"]!), + subtitle: Text(visibilityConfigurations[1]["description"]!), + ), + ), + Card( + child: ListTile( + leading: Checkbox( + value: checked[2], + onChanged: (bool? value) {}, + ), + title: Text(visibilityConfigurations[2]["type"]!), + subtitle: Text(visibilityConfigurations[2]["description"]!), + ), + ) + ], + ), + ), + actions: [ElevatedButton(onPressed: () {}, child: const Text("Save"))], + ); + } +} diff --git a/demos/supabase-trello/lib/features/workspace/domain/workspace_arguments.dart b/demos/supabase-trello/lib/features/workspace/domain/workspace_arguments.dart new file mode 100644 index 00000000..cdea7d12 --- /dev/null +++ b/demos/supabase-trello/lib/features/workspace/domain/workspace_arguments.dart @@ -0,0 +1,7 @@ +import 'package:trelloappclone_flutter/models/workspace.dart'; + +class WorkspaceArguments { + final Workspace wkspc; + + WorkspaceArguments(this.wkspc); +} diff --git a/demos/supabase-trello/lib/features/workspace/presentation/index.dart b/demos/supabase-trello/lib/features/workspace/presentation/index.dart new file mode 100644 index 00000000..4f23e377 --- /dev/null +++ b/demos/supabase-trello/lib/features/workspace/presentation/index.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/models/board.dart'; +import 'package:trelloappclone_flutter/features/drawer/presentation/index.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; + +import '../../../utils/service.dart'; +import '../domain/workspace_arguments.dart'; + +class WorkspaceScreen extends StatefulWidget { + const WorkspaceScreen({super.key}); + + @override + State createState() => _WorkspaceScreenState(); + + static const routeName = '/workspace'; +} + +class _WorkspaceScreenState extends State with Service { + @override + Widget build(BuildContext context) { + final args = + ModalRoute.of(context)!.settings.arguments as WorkspaceArguments; + + return Scaffold( + appBar: AppBar( + title: Text(args.wkspc.name), + actions: [ + IconButton(onPressed: () {}, icon: const Icon(Icons.search)), + IconButton( + onPressed: () {}, + icon: const Icon(Icons.notifications_none_outlined)) + ], + ), + drawer: const CustomDrawer(), + body: DefaultTabController( + length: 2, + initialIndex: 0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const TabBar( + labelColor: brandColor, + unselectedLabelColor: themeColor, + tabs: [ + Tab( + text: "BOARDS", + ), + Tab( + text: "HIGHLIGHTS", + ) + ]), + SizedBox( + height: MediaQuery.of(context).size.height * 0.8, + child: TabBarView(children: [ + Column( + children: [ + Container( + width: MediaQuery.of(context).size.width, + height: 50, + color: whiteShade, + alignment: Alignment.centerLeft, + child: const Padding( + padding: EdgeInsets.only(left: 8.0), + child: Text("Your Workspace boards")), + ), + StreamBuilder( + stream: getBoardsStream(args.wkspc.id), + builder: (context, snapshot) { + if (snapshot.hasData) { + List children = snapshot.data as List; + + if (children.isNotEmpty) { + return Expanded( + child: GridView.builder( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 1 / 0.7), + itemCount: children.length, + itemBuilder: + (BuildContext context, int index) { + return GestureDetector( + onTap: () {}, + child: Card( + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular( + 10.0)), + color: Color(int.parse( + children[index] + .background + .substring(1, 7), + radix: 16) + + 0xFF000000), + child: Align( + alignment: Alignment.bottomLeft, + child: ListTile( + tileColor: themeColor, + title: Text( + children[index].name, + style: const TextStyle( + color: Colors.white), + ), + ), + ), + ), + ); + })); + } + } + return const SizedBox.shrink(); + }, + ) + ], + ), + Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + children: [ + const ListTile( + leading: Icon( + Icons.start, + color: brandColor, + ), + title: Text("GET STARTED"), + ), + Card( + child: Column( + children: [ + Container( + color: brandColor, + height: 100, + ), + const Padding( + padding: EdgeInsets.all(8.0), + child: Text( + "Stay on track and up-to-date", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18), + ), + ), + const Padding( + padding: EdgeInsets.all(8.0), + child: Text( + "Invite people to boards and cards, add comments, and adjust due dates all from the new Trello Home. We'll show the most important activity here.", + textAlign: TextAlign.center, + ), + ) + ], + ), + ) + ], + )) + ]), + ) + ], + )), + ); + } +} diff --git a/demos/supabase-trello/lib/features/workspacemenu/presentation/index.dart b/demos/supabase-trello/lib/features/workspacemenu/presentation/index.dart new file mode 100644 index 00000000..051d3d8b --- /dev/null +++ b/demos/supabase-trello/lib/features/workspacemenu/presentation/index.dart @@ -0,0 +1,138 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/service.dart'; + +import '../../../main.dart'; +import '../../../utils/color.dart'; + +class WorkspaceMenu extends StatefulWidget { + const WorkspaceMenu({super.key}); + + @override + State createState() => _WorkspaceMenuState(); +} + +class _WorkspaceMenuState extends State with Service { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: const Icon( + Icons.close, + size: 30, + ), + ), + actions: [ + IconButton( + onPressed: () { + Navigator.pushNamed(context, '/workspacesettings'); + }, + icon: const Icon(Icons.settings)) + ], + title: const Text("Workspace menu"), + centerTitle: false, + ), + body: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + trello.selectedWorkspace.name, + style: const TextStyle( + fontWeight: FontWeight.bold, fontSize: 18), + ), + const Text.rich(TextSpan(children: [ + WidgetSpan( + child: + Icon(Icons.lock, color: dangerColor, size: 15)), + TextSpan( + text: "Public", + style: TextStyle(color: dangerColor)) + ])), + Padding( + padding: const EdgeInsets.only(top: 8.0, bottom: 8.0), + child: Text(trello.selectedWorkspace.description), + ), + ], + ), + const Spacer(), + CircleAvatar( + radius: 30, + backgroundColor: Colors.green[400], + child: Text( + trello.selectedWorkspace.name[0].toUpperCase(), + style: const TextStyle(color: whiteShade), + ), + ) + ], + ), + Container( + padding: const EdgeInsets.only(top: 10.0), + child: ListTile( + tileColor: whiteShade, + leading: const Icon(Icons.person_outline), + title: const Padding( + padding: EdgeInsets.only(top: 10.0, bottom: 15), + child: Text("Members"), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 18.0), + child: InkWell( + onTap: () { + Navigator.pushNamed(context, '/members'); + }, + child: Row( + children: buildMemberAvatars(), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 15.0), + child: SizedBox( + height: 37, + width: MediaQuery.of(context).size.width * 0.7, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: brandColor), + onPressed: () { + Navigator.pushNamed(context, '/invitemember'); + }, + child: const Text("Invite")), + ), + ) + ], + ), + ), + ) + ], + ), + )); + } + + List buildMemberAvatars() { + List avatars = []; + + trello.selectedWorkspace.members?.forEach((member) { + avatars.add(CircleAvatar( + backgroundColor: brandColor, + child: Text(member.name[0].toUpperCase()), + )); + avatars.add(const SizedBox( + width: 4, + )); + }); + return avatars; + } +} diff --git a/demos/supabase-trello/lib/features/workspacesettings/presentation/index.dart b/demos/supabase-trello/lib/features/workspacesettings/presentation/index.dart new file mode 100644 index 00000000..0a286fa1 --- /dev/null +++ b/demos/supabase-trello/lib/features/workspacesettings/presentation/index.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; + +import '../../../main.dart'; +import '../../visibility/presentation/index.dart'; + +class WorkspaceSettings extends StatefulWidget { + const WorkspaceSettings({super.key}); + + @override + State createState() => _WorkspaceSettingsState(); +} + +class _WorkspaceSettingsState extends State { + final TextEditingController nameController = TextEditingController(); + + @override + Widget build(BuildContext context) { + nameController.text = trello.selectedWorkspace.name; + return Scaffold( + appBar: AppBar( + title: const Text("Workspace settings"), + centerTitle: false, + ), + body: Column( + children: [ + ListTile( + leading: const Text("Name"), + trailing: SizedBox( + width: MediaQuery.of(context).size.width * 0.4, + child: EditableText( + textAlign: TextAlign.end, + controller: nameController, + focusNode: FocusNode(), + style: const TextStyle( + fontWeight: FontWeight.bold, color: brandColor), + cursorColor: brandColor, + backgroundCursorColor: brandColor, + onSubmitted: (value) { + Navigator.pushNamed(context, '/home'); + }, + )), + ), + ListTile( + leading: const Text("Visibility"), + trailing: GestureDetector( + child: const Text("Public"), + onTap: () { + showDialog( + context: context, + builder: (BuildContext context) { + return const BoardVisibility(); + }); + }, + ), + ), + const Align( + alignment: Alignment.center, + child: Text("Not all settings are editable on mobile"), + ) + ], + ), + ); + } +} diff --git a/demos/supabase-trello/lib/main.dart b/demos/supabase-trello/lib/main.dart new file mode 100644 index 00000000..139e95fb --- /dev/null +++ b/demos/supabase-trello/lib/main.dart @@ -0,0 +1,117 @@ +// ignore_for_file: avoid_print + +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:provider/provider.dart'; +import 'package:trelloappclone_flutter/features/generateworkspace/presentation/index.dart'; +import 'package:trelloappclone_flutter/utils/trello_provider.dart'; +import 'package:trelloappclone_flutter/protocol/data_client.dart'; +import 'package:logging/logging.dart'; + +import 'features/aboutboard/presentation/index.dart'; +import 'features/archivedcards/presentation/index.dart'; +import 'features/archivedlists/presentation/index.dart'; +import 'features/board/presentation/index.dart'; +import 'features/boardbackground/presentation/index.dart'; +import 'features/boardmenu/presentation/index.dart'; +import 'features/boardsettings/presentation/index.dart'; +import 'features/carddetails/presentation/index.dart'; +import 'features/copyboard/presentation/index.dart'; +import 'features/createboard/presentation/index.dart'; +import 'features/createcard/presentation/index.dart'; +import 'features/createworkspace/presentation/index.dart'; +import 'features/emailtoboard/presentation/index.dart'; +import 'features/home/presentation/index.dart'; +import 'features/invitemember/presentation/index.dart'; +import 'features/landing/presentation/index.dart'; +import 'features/members/presentation/index.dart'; +import 'features/mycards/presentation/index.dart'; +import 'features/notifications/presentation/index.dart'; +import 'features/offlineboards/presentation/index.dart'; +import 'features/powerups/presentation/index.dart'; +import 'features/settings/presentation/index.dart'; +import 'features/signtotrello/presentation/index.dart'; +import 'features/workspace/presentation/index.dart'; +import 'features/workspacemenu/presentation/index.dart'; +import 'features/workspacesettings/presentation/index.dart'; + +// Sets up a singleton client object that can be used to talk to the server from +// anywhere in our app. +// The client is set up to connect to a Powersync project already set up. +var dataClient = DataClient(); + +TrelloProvider trello = TrelloProvider(); + +void main() async { + // Log info from PowerSync + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { + // if (kDebugMode) { + print( + '[${record.loggerName}] ${record.level.name}: ${record.time}: ${record.message}'); + + if (record.error != null) { + print(record.error); + } + if (record.stackTrace != null) { + print(record.stackTrace); + } + // } + }); + + WidgetsFlutterBinding + .ensureInitialized(); //required to get sqlite filepath from path_provider before UI has initialized + await dataClient.initialize(); + if (dataClient.isLoggedIn()) { + TrelloUser? user = await dataClient.getLoggedInUser(); + trello.setUser(user!); + } + runApp(ChangeNotifierProvider( + create: (context) => TrelloProvider(), child: const MyApp())); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Trello App Clone', + theme: ThemeData( + textTheme: Theme.of(context).textTheme.apply( + fontFamily: GoogleFonts.poppins().fontFamily, + ), + ), + initialRoute: dataClient.isLoggedIn() ? '/home' : '/', + routes: { + '/': (context) => const Landing(), + '/home': (context) => const Home(), + '/notifications': (context) => const Notifications(), + '/workspacemenu': (context) => const WorkspaceMenu(), + '/workspacesettings': (context) => const WorkspaceSettings(), + '/members': (context) => const Members(), + '/invitemember': (context) => const InviteMember(), + '/createworkspace': (context) => const CreateWorkspace(), + '/createboard': (context) => const CreateBoard(), + '/generateworkspace': (context) => const GenerateWorkspace(), + '/boardbackground': (context) => const BoardBackground(), + '/createcard': (context) => const CreateCard(), + BoardScreen.routeName: (context) => const BoardScreen(), + '/boardmenu': (context) => const BoardMenu(), + '/copyboard': (context) => const CopyBoard(), + '/boardsettings': (context) => const BoardSettings(), + '/archivedlists': (context) => const ArchivedLists(), + '/archivedcards': (context) => const ArchivedCards(), + '/emailtoboard': (context) => const EmailToBoard(), + '/aboutboard': (context) => const AboutBoard(), + '/powerups': (context) => const PowerUps(), + CardDetails.routeName: (context) => const CardDetails(), + '/mycards': (context) => const MyCards(), + '/offlineboards': (context) => const OfflineBoards(), + '/settings': (context) => const Settings(), + SignToTrello.routeName: (context) => const SignToTrello(), + '/workspace': (context) => const WorkspaceScreen() + }); + } +} diff --git a/demos/supabase-trello/lib/models/activity.dart b/demos/supabase-trello/lib/models/activity.dart new file mode 100644 index 00000000..ce85f190 --- /dev/null +++ b/demos/supabase-trello/lib/models/activity.dart @@ -0,0 +1,37 @@ +import 'package:powersync/sqlite3.dart' as sqlite; + +class Activity { + final String id; + final String workspaceId; + + final String? boardId; + + final String userId; + + final String? cardId; + + final String description; + + final DateTime dateCreated; + + Activity({ + required this.id, + required this.workspaceId, + this.boardId, + required this.userId, + this.cardId, + required this.description, + required this.dateCreated, + }); + + factory Activity.fromRow(sqlite.Row row) { + return Activity( + id: row['id'], + workspaceId: row['workspaceId'], + boardId: row['boardId'], + userId: row['userId'], + cardId: row['cardId'], + description: row['description'], + dateCreated: DateTime.parse(row['dateCreated'])); + } +} diff --git a/demos/supabase-trello/lib/models/attachment.dart b/demos/supabase-trello/lib/models/attachment.dart new file mode 100644 index 00000000..1ef86d15 --- /dev/null +++ b/demos/supabase-trello/lib/models/attachment.dart @@ -0,0 +1,29 @@ +import 'package:powersync/sqlite3.dart' as sqlite; + +class Attachment { + Attachment({ + required this.id, + required this.workspaceId, + required this.userId, + required this.cardId, + required this.attachment, + }); + + final String id; + final String workspaceId; + + final String userId; + + final String cardId; + + final String attachment; + + factory Attachment.fromRow(sqlite.Row row) { + return Attachment( + id: row['id'], + workspaceId: row['workspaceId'], + userId: row['userId'], + cardId: row['cardId'], + attachment: row['attachment']); + } +} diff --git a/demos/supabase-trello/lib/models/board.dart b/demos/supabase-trello/lib/models/board.dart new file mode 100644 index 00000000..a9bbe8f8 --- /dev/null +++ b/demos/supabase-trello/lib/models/board.dart @@ -0,0 +1,86 @@ +import 'package:powersync/sqlite3.dart' as sqlite; +import 'package:trelloappclone_flutter/models/board_label.dart'; + +class Board { + Board( + {required this.id, + required this.workspaceId, + required this.userId, + required this.name, + this.description, + required this.visibility, + required this.background, + this.starred, + this.enableCover, + this.watch, + this.availableOffline, + this.label, + this.emailAddress, + this.commenting, + this.memberType, + this.pinned, + this.selfJoin, + this.close, + this.boardLabels}); + + final String id; + + final String workspaceId; + + final String userId; + + final String name; + + final String? description; + + final String visibility; + + final String background; + + final bool? starred; + + final bool? enableCover; + + final bool? watch; + + bool? availableOffline; + + final String? label; + + final String? emailAddress; + + final int? commenting; + + final int? memberType; + + final bool? pinned; + + final bool? selfJoin; + + final bool? close; + + List? boardLabels; + + factory Board.fromRow(sqlite.Row row) { + return Board( + id: row['id'], + workspaceId: row['workspaceId'], + userId: row['userId'], + name: row['name'], + description: row['description'], + visibility: row['visibility'], + background: row['background'], + starred: row['starred'] == 1, + enableCover: row['enableCover'] == 1, + watch: row['watch'] == 1, + availableOffline: row['availableOffline'] == 1, + label: row['label'], + emailAddress: row['emailAddress'], + commenting: row['commenting'], + memberType: row['memberType'], + pinned: row['pinned'] == 1, + selfJoin: row['selfJoin'] == 1, + close: row['close'] == 1, + boardLabels: []); + } +} diff --git a/demos/supabase-trello/lib/models/board_label.dart b/demos/supabase-trello/lib/models/board_label.dart new file mode 100644 index 00000000..5b63fcee --- /dev/null +++ b/demos/supabase-trello/lib/models/board_label.dart @@ -0,0 +1,34 @@ +import 'package:powersync/sqlite3.dart' as sqlite; + +class BoardLabel { + final String id; + + final String boardId; + + final String workspaceId; + + late String title; + + final String color; + + final DateTime dateCreated; + + BoardLabel({ + required this.id, + required this.boardId, + required this.workspaceId, + required this.title, + required this.color, + required this.dateCreated, + }); + + factory BoardLabel.fromRow(sqlite.Row row) { + return BoardLabel( + id: row['id'], + boardId: row['boardId'], + workspaceId: row['workspaceId'], + title: row['title'], + color: row['color'], + dateCreated: DateTime.parse(row['dateCreated'])); + } +} diff --git a/demos/supabase-trello/lib/models/card.dart b/demos/supabase-trello/lib/models/card.dart new file mode 100644 index 00000000..39ff31f0 --- /dev/null +++ b/demos/supabase-trello/lib/models/card.dart @@ -0,0 +1,68 @@ +import 'package:powersync/sqlite3.dart' as sqlite; +import 'package:trelloappclone_flutter/models/card_label.dart'; + +class Cardlist { + Cardlist({ + required this.id, + required this.workspaceId, + required this.listId, + required this.userId, + required this.name, + this.description, + this.startDate, + this.dueDate, + required this.rank, + this.attachment, + this.archived, + this.checklist, + this.comments, + this.cardLabels, + }); + + final String id; + + final String workspaceId; + + String listId; + + final String userId; + + String name; + + String? description; + + final DateTime? startDate; + + final DateTime? dueDate; + + int rank; + + final bool? attachment; + + final bool? archived; + + final bool? checklist; + + final bool? comments; + + List? cardLabels; + + factory Cardlist.fromRow(sqlite.Row row) { + return Cardlist( + id: row['id'], + workspaceId: row['workspaceId'], + listId: row['listId'], + userId: row['userId'], + name: row['name'], + description: row['description'], + startDate: + row['startDate'] != null ? DateTime.parse(row['startDate']) : null, + dueDate: row['dueDate'] != null ? DateTime.parse(row['dueDate']) : null, + rank: row['rank'], + attachment: row['attachment'] == 1, + archived: row['archived'] == 1, + checklist: row['checklist'] == 1, + comments: row['comments'] == 1, + cardLabels: []); + } +} diff --git a/demos/supabase-trello/lib/models/card_label.dart b/demos/supabase-trello/lib/models/card_label.dart new file mode 100644 index 00000000..6016e150 --- /dev/null +++ b/demos/supabase-trello/lib/models/card_label.dart @@ -0,0 +1,34 @@ +import 'package:powersync/sqlite3.dart' as sqlite; + +class CardLabel { + final String id; + + final String cardId; + + final String boardId; + + final String workspaceId; + + final String boardLabelId; + + final DateTime dateCreated; + + CardLabel({ + required this.id, + required this.cardId, + required this.boardId, + required this.workspaceId, + required this.boardLabelId, + required this.dateCreated, + }); + + factory CardLabel.fromRow(sqlite.Row row) { + return CardLabel( + id: row['id'], + cardId: row['cardId'], + boardId: row['boardId'], + workspaceId: row['workspaceId'], + boardLabelId: row['boardLabelId'], + dateCreated: DateTime.parse(row['dateCreated'])); + } +} diff --git a/demos/supabase-trello/lib/models/checklist.dart b/demos/supabase-trello/lib/models/checklist.dart new file mode 100644 index 00000000..afbc969b --- /dev/null +++ b/demos/supabase-trello/lib/models/checklist.dart @@ -0,0 +1,30 @@ +import 'package:powersync/sqlite3.dart' as sqlite; + +class Checklist { + Checklist({ + required this.id, + required this.workspaceId, + required this.cardId, + required this.name, + required this.status, + }); + + final String id; + + final String workspaceId; + + final String cardId; + + final String name; + + bool status; + + factory Checklist.fromRow(sqlite.Row row) { + return Checklist( + id: row['id'], + workspaceId: row['workspaceId'], + cardId: row['cardId'], + name: row['name'], + status: row['status'] == 1); + } +} diff --git a/demos/supabase-trello/lib/models/comment.dart b/demos/supabase-trello/lib/models/comment.dart new file mode 100644 index 00000000..878f47ae --- /dev/null +++ b/demos/supabase-trello/lib/models/comment.dart @@ -0,0 +1,30 @@ +import 'package:powersync/sqlite3.dart' as sqlite; + +class Comment { + Comment({ + required this.id, + required this.workspaceId, + required this.cardId, + required this.userId, + required this.description, + }); + + final String id; + + final String workspaceId; + + final String cardId; + + final String userId; + + final String description; + + factory Comment.fromRow(sqlite.Row row) { + return Comment( + id: row['id'], + workspaceId: row['workspaceId'], + cardId: row['cardId'], + userId: row['userId'], + description: row['description']); + } +} diff --git a/demos/supabase-trello/lib/models/listboard.dart b/demos/supabase-trello/lib/models/listboard.dart new file mode 100644 index 00000000..887ddcd9 --- /dev/null +++ b/demos/supabase-trello/lib/models/listboard.dart @@ -0,0 +1,43 @@ +import 'package:powersync/sqlite3.dart' as sqlite; +import 'card.dart'; + +class Listboard { + Listboard({ + required this.id, + required this.workspaceId, + required this.boardId, + required this.userId, + required this.name, + this.archived, + this.cards, + required this.order, + }); + + final String id; + + final String workspaceId; + + final String boardId; + + final String userId; + + final String name; + + final bool? archived; + + final int order; + + List? cards; + + factory Listboard.fromRow(sqlite.Row row) { + return Listboard( + id: row['id'], + workspaceId: row['workspaceId'], + boardId: row['boardId'], + userId: row['userId'], + name: row['name'], + archived: row['archived'] == 1, + order: row['listOrder'], + cards: []); + } +} diff --git a/demos/supabase-trello/lib/models/member.dart b/demos/supabase-trello/lib/models/member.dart new file mode 100644 index 00000000..ef90436b --- /dev/null +++ b/demos/supabase-trello/lib/models/member.dart @@ -0,0 +1,30 @@ +import 'package:powersync/sqlite3.dart' as sqlite; + +class Member { + Member({ + required this.id, + required this.workspaceId, + required this.userId, + required this.name, + required this.role, + }); + + final String id; + + final String workspaceId; + + final String userId; + + final String name; + + final String role; + + factory Member.fromRow(sqlite.Row row) { + return Member( + id: row['id'], + workspaceId: row['workspaceId'], + userId: row['userId'], + name: row['name'], + role: row['role']); + } +} diff --git a/demos/supabase-trello/lib/models/models.dart b/demos/supabase-trello/lib/models/models.dart new file mode 100644 index 00000000..d824c549 --- /dev/null +++ b/demos/supabase-trello/lib/models/models.dart @@ -0,0 +1,12 @@ +export 'activity.dart'; +export 'attachment.dart'; +export 'board.dart'; +export 'card.dart'; +export 'checklist.dart'; +export 'comment.dart'; +export 'listboard.dart'; +export 'member.dart'; +export 'user.dart'; +export 'workspace.dart'; +export 'board_label.dart'; +export 'card_label.dart'; diff --git a/demos/supabase-trello/lib/models/schema.dart b/demos/supabase-trello/lib/models/schema.dart new file mode 100644 index 00000000..b093680e --- /dev/null +++ b/demos/supabase-trello/lib/models/schema.dart @@ -0,0 +1,149 @@ +import 'package:powersync/powersync.dart'; + +const schema = Schema(([ + //class: Activity + Table('activity', [ + Column.text('workspaceId'), + Column.text('boardId'), + Column.text('userId'), + Column.text('cardId'), + Column.text('description'), + Column.text('dateCreated'), + ], indexes: [ + Index('board', [IndexedColumn('boardId')]), + Index('user', [IndexedColumn('userId')]), + Index('card', [IndexedColumn('cardId')]) + ]), + //class: Attachment + Table('attachment', [ + Column.text('workspaceId'), + Column.text('userId'), + Column.text('cardId'), + Column.text('attachment'), + ], indexes: [ + Index('user', [IndexedColumn('userId')]), + ]), + //class: Board + Table('board', [ + Column.text('workspaceId'), + Column.text('userId'), + Column.text('name'), + Column.text('description'), + Column.text('visibility'), + Column.text('background'), + Column.integer('starred'), + Column.integer('enableCover'), + Column.integer('watch'), + Column.integer('availableOffline'), + Column.text('label'), + Column.text('emailAddress'), + Column.integer('commenting'), + Column.integer('memberType'), + Column.integer('pinned'), + Column.integer('selfJoin'), + Column.integer('close'), + ], indexes: [ + Index('workspace', [IndexedColumn('workspaceId')]), + Index('user', [IndexedColumn('userId')]), + ]), + //class: Cardlist + Table('card', [ + Column.text('workspaceId'), + Column.text('listId'), + Column.text('userId'), + Column.text('name'), + Column.text('description'), + Column.text('startDate'), + Column.text('dueDate'), + Column.integer('rank'), + Column.integer('attachment'), + Column.integer('archived'), + Column.integer('checklist'), + Column.integer('comments'), + ], indexes: [ + Index('list', [IndexedColumn('listId')]), + Index('user', [IndexedColumn('userId')]), + ]), + //class: Checklist + Table('checklist', [ + Column.text('workspaceId'), + Column.text('cardId'), + Column.text('name'), + Column.integer('status'), + ], indexes: [ + Index('card', [IndexedColumn('cardId')]), + ]), + //class: Comment + Table('comment', [ + Column.text('workspaceId'), + Column.text('cardId'), + Column.text('userId'), + Column.text('description'), + ], indexes: [ + Index('card', [IndexedColumn('cardId')]), + Index('user', [IndexedColumn('userId')]), + ]), + //class: Listboard + Table('listboard', [ + Column.text('workspaceId'), + Column.text('boardId'), + Column.text('userId'), + Column.text('name'), + Column.integer('archived'), + Column.integer('listOrder'), + ], indexes: [ + Index('board', [IndexedColumn('boardId')]), + Index('user', [IndexedColumn('userId')]), + ]), + //class: Member + Table('member', [ + Column.text('workspaceId'), + Column.text('userId'), + Column.text('name'), + Column.text('role'), + ], indexes: [ + Index('user', [IndexedColumn('userId')]), + ]), + //class: User + // table: trellouser + // fields: + // name: String? + // email: String + // password: String + Table('trellouser', [ + Column.text('name'), + Column.text('email'), + Column.text('password'), + ], indexes: [ + Index('email', [IndexedColumn('email')]), + ]), + //class: Workspace + Table('workspace', [ + Column.text('userId'), + Column.text('name'), + Column.text('description'), + Column.text('visibility'), + ], indexes: [ + Index('user', [IndexedColumn('userId')]), + ]), + // class: BoardLabel + Table('board_label', [ + Column.text('boardId'), + Column.text('workspaceId'), + Column.text('title'), + Column.text('color'), + Column.text('dateCreated'), + ], indexes: [ + Index('board', [IndexedColumn('boardId')]), + ]), + // class: CardLabel + Table('card_label', [ + Column.text('cardId'), + Column.text('boardLabelId'), + Column.text('boardId'), + Column.text('workspaceId'), + Column.text('dateCreated'), + ], indexes: [ + Index('card', [IndexedColumn('cardId')]), + ]) +])); diff --git a/demos/supabase-trello/lib/models/user.dart b/demos/supabase-trello/lib/models/user.dart new file mode 100644 index 00000000..0eec95dc --- /dev/null +++ b/demos/supabase-trello/lib/models/user.dart @@ -0,0 +1,26 @@ +import 'package:powersync/sqlite3.dart' as sqlite; + +class TrelloUser { + TrelloUser({ + required this.id, + this.name, + required this.email, + required this.password, + }); + + final String id; + + final String? name; + + final String email; + + final String password; + + factory TrelloUser.fromRow(sqlite.Row row) { + return TrelloUser( + id: row['id'], + name: row['name'], + email: row['email'], + password: row['password']); + } +} diff --git a/demos/supabase-trello/lib/models/workspace.dart b/demos/supabase-trello/lib/models/workspace.dart new file mode 100644 index 00000000..d5e194a6 --- /dev/null +++ b/demos/supabase-trello/lib/models/workspace.dart @@ -0,0 +1,36 @@ +import 'package:powersync/sqlite3.dart' as sqlite; + +import 'member.dart'; + +class Workspace { + Workspace({ + required this.id, + required this.userId, + required this.name, + required this.description, + required this.visibility, + this.members, + }); + + final String id; + + final String userId; + + final String name; + + final String description; + + final String visibility; + + List? members; + + factory Workspace.fromRow(sqlite.Row row) { + return Workspace( + id: row['id'], + userId: row['userId'], + name: row['name'], + description: row['description'], + visibility: row['visibility'], + members: []); + } +} diff --git a/demos/supabase-trello/lib/protocol/data_client.dart b/demos/supabase-trello/lib/protocol/data_client.dart new file mode 100644 index 00000000..c7af10d5 --- /dev/null +++ b/demos/supabase-trello/lib/protocol/data_client.dart @@ -0,0 +1,855 @@ +// ignore_for_file: library_private_types_in_public_api + +library powersync_client; + +import 'package:powersync/powersync.dart'; + +import "../models/models.dart"; +import 'powersync.dart'; + +export "../models/models.dart"; + +class _Repository { + DataClient client; + + _Repository(this.client); + + int boolAsInt(bool? value) { + if (value == null) { + return 0; + } + return value ? 1 : 0; + } +} + +class _ActivityRepository extends _Repository { + _ActivityRepository(super.client); + + Future createActivity(Activity activity) async { + final results = await client.getDBExecutor().execute('''INSERT INTO + activity(id, workspaceId, boardId, userId, cardId, description, dateCreated) + VALUES(?, ?, ?, ?, ?, ?, datetime()) + RETURNING *''', [ + activity.id, + activity.workspaceId, + activity.boardId, + activity.userId, + activity.cardId, + activity.description + ]); + return results.isNotEmpty; + } + + Future> getActivities(Cardlist cardlist) async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM activity WHERE cardId = ? ORDER BY dateCreated DESC + ''', [cardlist.id]); + return results.map((row) => Activity.fromRow(row)).toList(); + } +} + +class _AttachmentRepository extends _Repository { + _AttachmentRepository(super.client); + + Future addAttachment(Attachment attachment) async { + final results = await client.getDBExecutor().execute('''INSERT INTO + attachment(id, workspaceId, userId, cardId, attachment) + VALUES(?, ?, ?, ?, ?) + RETURNING *''', [ + attachment.id, + attachment.workspaceId, + attachment.userId, + attachment.cardId, + attachment.attachment + ]); + if (results.isEmpty) { + throw Exception("Failed to add attachment"); + } else { + return Attachment.fromRow(results.first); + } + } + + //TODO: need to replace with file upload service calls to Supabase + Future getUploadDescription(String path) => + Future.value('TODO: implement getUploadDescription'); + + Future verifyUpload(String path) => Future.value(false); +} + +class _BoardRepository extends _Repository { + _BoardRepository(super.client); + + String get insertQuery => ''' + INSERT INTO + board(id, userId, workspaceId, name, description, visibility, background, starred, enableCover, watch, availableOffline, label, emailAddress, commenting, memberType, pinned, selfJoin, close) + VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18) + '''; + + String get updateQuery => ''' + UPDATE board + set userId = ?1, workspaceId = ?2, name = ?3, description = ?4, visibility = ?5, background = ?6, starred = ?7, enableCover = ?8, watch = ?9, availableOffline = ?10, label = ?11, emailAddress = ?12, commenting = ?13, memberType = ?14, pinned = ?15, selfJoin = ?16, close = ?17 + WHERE id = ?18 + '''; + + Future createBoard(Board board) async { + final results = await client.getDBExecutor().execute(''' + $insertQuery RETURNING *''', [ + board.id, + board.userId, + board.workspaceId, + board.name, + board.description, + board.visibility, + board.background, + boolAsInt(board.starred), + boolAsInt(board.enableCover), + boolAsInt(board.watch), + boolAsInt(board.availableOffline), + board.label, + board.emailAddress, + board.commenting, + board.memberType, + boolAsInt(board.pinned), + boolAsInt(board.selfJoin), + boolAsInt(board.close) + ]); + if (results.isEmpty) { + throw Exception("Failed to add Board"); + } else { + return Board.fromRow(results.first); + } + } + + Future updateBoard(Board board) async { + await client.getDBExecutor().execute(updateQuery, [ + board.userId, + board.workspaceId, + board.name, + board.description, + board.visibility, + board.background, + boolAsInt(board.starred), + boolAsInt(board.enableCover), + boolAsInt(board.watch), + boolAsInt(board.availableOffline), + board.label, + board.emailAddress, + board.commenting, + board.memberType, + boolAsInt(board.pinned), + boolAsInt(board.selfJoin), + boolAsInt(board.close), + board.id + ]); + return true; + } + + Future deleteBoard(Board board) async { + await client + .getDBExecutor() + .execute('DELETE FROM board WHERE id = ?', [board.id]); + return true; + } + + Future getWorkspaceForBoard(Board board) async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM workspace WHERE id = ? + ''', [board.workspaceId]); + return results.map((row) => Workspace.fromRow(row)).firstOrNull; + } + + Future> getAllBoards() async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM board + '''); + return results.map((row) => Board.fromRow(row)).toList(); + } +} + +class _CardlistRepository extends _Repository { + _CardlistRepository(super.client); + + String get insertQuery => ''' + INSERT INTO + card(id, workspaceId, listId, userId, name, description, startDate, dueDate, attachment, archived, checklist, comments, rank) + VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13) + '''; + + Future createCard(Cardlist cardlist) async { + final results = + await client.getDBExecutor().execute('$insertQuery RETURNING *', [ + cardlist.id, + cardlist.workspaceId, + cardlist.listId, + cardlist.userId, + cardlist.name, + cardlist.description, + cardlist.startDate, + cardlist.dueDate, + boolAsInt(cardlist.attachment), + boolAsInt(cardlist.archived), + boolAsInt(cardlist.checklist), + boolAsInt(cardlist.comments), + cardlist.rank, + ]); + if (results.isEmpty) { + throw Exception("Failed to add Cardlist"); + } else { + return Cardlist.fromRow(results.first); + } + } + + String get updateQuery => ''' + UPDATE card + set listId = ?1, userId = ?2, name = ?3, description = ?4, startDate = ?5, dueDate = ?6, attachment = ?7, archived = ?8, checklist = ?9, comments = ?10, rank = ?11 + WHERE id = ?12 + '''; + + Future updateCard(Cardlist cardlist) async { + await client.getDBExecutor().execute(updateQuery, [ + cardlist.listId, + cardlist.userId, + cardlist.name, + cardlist.description, + cardlist.startDate, + cardlist.dueDate, + boolAsInt(cardlist.attachment), + boolAsInt(cardlist.archived), + boolAsInt(cardlist.checklist), + boolAsInt(cardlist.comments), + cardlist.rank, + cardlist.id + ]); + return true; + } + + Future deleteCard(Cardlist cardlist) async { + await client + .getDBExecutor() + .execute('DELETE FROM card WHERE id = ?', [cardlist.id]); + return true; + } + + Future> getCardsforList(String listId) async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM card WHERE listId = ? AND archived = 0 ORDER BY rank ASC + ''', [listId]); + return results.map((row) => Cardlist.fromRow(row)).toList(); + } +} + +class _CheckListRepository extends _Repository { + _CheckListRepository(super.client); + + String get insertQuery => ''' + INSERT INTO + checklist(id, workspaceId, cardId, name, status) + VALUES(?1, ?2, ?3, ?4, ?5) + '''; + + Future createChecklist(Checklist checklist) async { + final results = + await client.getDBExecutor().execute('$insertQuery RETURNING *', [ + checklist.id, + checklist.workspaceId, + checklist.cardId, + checklist.name, + boolAsInt(checklist.status) + ]); + if (results.isEmpty) { + throw Exception("Failed to add Checklist"); + } else { + return Checklist.fromRow(results.first); + } + } + + String get updateQuery => ''' + UPDATE checklist + set cardId = ?1, name = ?2, status = ?3 + WHERE id = ?4 + '''; + + Future updateChecklist(Checklist checklist) async { + await client.getDBExecutor().execute(updateQuery, [ + checklist.cardId, + checklist.name, + boolAsInt(checklist.status), + checklist.id + ]); + return true; + } + + Future deleteChecklistItem(Checklist checklist) async { + await client + .getDBExecutor() + .execute('DELETE FROM checklist WHERE id = ?', [checklist.id]); + return true; + } + + Future> getChecklists(Cardlist crd) async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM checklist WHERE cardId = ? + ''', [crd.id]); + return results.map((row) => Checklist.fromRow(row)).toList(); + } + + Future deleteChecklist(Cardlist crd) async { + final results = await client.getDBExecutor().execute(''' + SELECT COUNT(*) FROM checklist WHERE cardId = ? + ''', [crd.id]); + await client + .getDBExecutor() + .execute('DELETE FROM checklist WHERE cardId = ?', [crd.id]); + return results.first['count']; + } +} + +class _CommentRepository extends _Repository { + _CommentRepository(super.client); + + String get insertQuery => ''' + INSERT INTO + comment(id, workspaceId, cardId, userId, description) + VALUES(?1, ?2, ?3, ?4, ?5) + '''; + + Future createComment(Comment comment) async { + final results = + await client.getDBExecutor().execute('$insertQuery RETURNING *', [ + comment.id, + comment.workspaceId, + comment.cardId, + comment.userId, + comment.description + ]); + if (results.isEmpty) { + throw Exception("Failed to add Comment"); + } else { + return Comment.fromRow(results.first); + } + } + + String get updateQuery => ''' + UPDATE comment + set cardId = ?1, userId = ?2, description = ?3 + WHERE id = ?4 + '''; + + Future updateComment(Comment comment) async { + await client.getDBExecutor().execute(updateQuery, + [comment.cardId, comment.userId, comment.description, comment.id]); + return true; + } +} + +class _ListboardRepository extends _Repository { + _ListboardRepository(super.client); + + Future> getListsByBoard({required String boardId}) async { + //first we get the listboards + final results = await client.getDBExecutor().execute(''' + SELECT * FROM listboard WHERE boardId = ? + ''', [boardId]); + List lists = + results.map((row) => Listboard.fromRow(row)).toList(); + + //then we set the cards for each listboard + for (Listboard list in lists) { + List cards = await client.card.getCardsforList(list.id); + list.cards = cards; + + for (Cardlist card in list.cards!) { + List labels = await client.cardLabel.getCardLabels(card); + card.cardLabels = labels; + } + } + + return lists; + } + + Stream> watchListsByBoard({required String boardId}) { + //first we get the listboards + return client.getDBExecutor().watch(''' + SELECT * FROM listboard WHERE boardId = ? ORDER BY listOrder ASC + ''', parameters: [boardId]).asyncMap((event) async { + List lists = + event.map((row) => Listboard.fromRow(row)).toList(); + + //then we set the cards for each listboard + for (Listboard list in lists) { + List cards = await client.card.getCardsforList(list.id); + list.cards = cards; + + for (Cardlist card in list.cards!) { + List labels = await client.cardLabel.getCardLabels(card); + card.cardLabels = labels; + } + } + + return lists; + }); + } + + String get insertQuery => ''' + INSERT INTO + listboard(id, workspaceId, boardId, userId, name, archived, listOrder) + VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7) + '''; + + Future createList(Listboard lst) async { + final results = + await client.getDBExecutor().execute('$insertQuery RETURNING *', [ + lst.id, + lst.workspaceId, + lst.boardId, + lst.userId, + lst.name, + boolAsInt(lst.archived), + lst.order + ]); + if (results.isEmpty) { + throw Exception("Failed to add Listboard"); + } else { + return Listboard.fromRow(results.first); + } + } + + String get updateQuery => ''' + UPDATE listboard + set listOrder = ?1 + WHERE id = ?2 + '''; + + Future updateListOrder(String listId, int newOrder) async { + await client.getDBExecutor().execute(updateQuery, [newOrder, listId]); + } + + /// Archive cards in and return how many were archived + /// This happens in a transaction + Future archiveCardsInList(Listboard list) async { + if (list.cards == null || list.cards!.isEmpty) { + return 0; + } + + //start transaction + return client.getDBExecutor().writeTransaction((sqlContext) async { + List cards = list.cards!; + int numCards = cards.length; + + //we set each of the cards in the list to archived = true + sqlContext.executeBatch(''' + UPDATE card + SET archived = 1 + WHERE id = ? + ''', cards.map((card) => [card.id]).toList()); + + //touch listboard to trigger update via stream listeners on Listboard + sqlContext.execute(''' + UPDATE listboard + SET archived = 0 + WHERE id = ? + ''', [list.id]); + + list.cards = []; + return numCards; + //end of transaction + }); + } +} + +class _MemberRepository extends _Repository { + _MemberRepository(super.client); + + String get insertQuery => ''' + INSERT INTO + member(id, workspaceId, userId, name, role) + VALUES(?1, ?2, ?3, ?4, ?5) + '''; + + Future addMember(Member member) async { + final results = await client.getDBExecutor().execute( + '$insertQuery RETURNING *', [ + member.id, + member.workspaceId, + member.userId, + member.name, + member.role + ]); + if (results.isEmpty) { + throw Exception("Failed to add Member"); + } else { + return Member.fromRow(results.first); + } + } + + Future> getMembersByWorkspace( + {required String workspaceId}) async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM member WHERE workspaceId = ? + ''', [workspaceId]); + return results.map((row) => Member.fromRow(row)).toList(); + } + + Future> getInformationOfMembers(List members) async { + List users = []; + for (Member member in members) { + TrelloUser? user = await client.user.getUserById(userId: member.userId); + if (user != null) { + users.add(user); + } + } + return users; + } + + Future deleteMember(Member member, Workspace workspace) async { + //delete member + await client.getDBExecutor().execute( + 'DELETE FROM member WHERE workspaceId = ? AND id = ?', + [workspace.id, member.id]); + + //update workspace list with new members + List newMembersList = + await getMembersByWorkspace(workspaceId: workspace.id); + workspace.members = newMembersList; + return workspace; + } +} + +class _UserRepository extends _Repository { + _UserRepository(super.client); + + String get insertQuery => ''' + INSERT INTO + trellouser(id, name, email, password) + VALUES(?1, ?2, ?3, ?4) + '''; + + Future createUser(TrelloUser user) async { + final results = await client.getDBExecutor().execute( + '$insertQuery RETURNING *', + [user.id, user.name, user.email, user.password]); + if (results.isEmpty) { + throw Exception("Failed to add User"); + } else { + return TrelloUser.fromRow(results.first); + } + } + + /// We excpect only one record in the local trellouser table + /// if somebody has logged in and not logged out again + Future getUser() async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM trellouser'''); + return results.map((row) => TrelloUser.fromRow(row)).firstOrNull; + } + + Future getUserById({required String userId}) async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM trellouser WHERE id = ? + ''', [userId]); + return results.map((row) => TrelloUser.fromRow(row)).firstOrNull; + } + + Future checkUserExists(String email) async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM trellouser WHERE email = ? + ''', [email]); + return results.map((row) => TrelloUser.fromRow(row)).firstOrNull; + } +} + +class _WorkspaceRepository extends _Repository { + _WorkspaceRepository(super.client); + + String get insertQuery => ''' + INSERT INTO + workspace(id, userId, name, description, visibility) + VALUES(?1, ?2, ?3, ?4, ?5) + '''; + + Future createWorkspace(Workspace workspace) async { + final results = + await client.getDBExecutor().execute('$insertQuery RETURNING *', [ + workspace.id, + workspace.userId, + workspace.name, + workspace.description, + workspace.visibility + ]); + return Workspace.fromRow(results.first); + } + + Future> getWorkspacesByUser({required String userId}) async { + //First we get the workspaces + final results = await client.getDBExecutor().execute(''' + SELECT * FROM workspace WHERE userId = ? + ''', [userId]); + List workspaces = + results.map((row) => Workspace.fromRow(row)).toList(); + + //Then we get the members for each workspace + for (Workspace workspace in workspaces) { + List members = + await client.member.getMembersByWorkspace(workspaceId: workspace.id); + workspace.members = members; + } + + return workspaces; + } + + Stream> watchWorkspacesByUser({required String userId}) { + //First we get the workspaces + return client.getDBExecutor().watch( + ''' + SELECT * FROM workspace + ''', + ).asyncMap((event) async { + List workspaces = + event.map((row) => Workspace.fromRow(row)).toList(); + + //Then we get the members for each workspace + for (Workspace workspace in workspaces) { + List members = await client.member + .getMembersByWorkspace(workspaceId: workspace.id); + workspace.members = members; + } + return workspaces; + }); + } + + Future getWorkspaceById({required String workspaceId}) async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM workspace WHERE id = ? + ''', [workspaceId]); + Workspace workspace = Workspace.fromRow(results.first); + List members = + await client.member.getMembersByWorkspace(workspaceId: workspaceId); + workspace.members = members; + return workspace; + } + + Future> getBoardsByWorkspace( + {required String workspaceId}) async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM board WHERE workspaceId = ? + ''', [workspaceId]); + + List boards = results.map((row) => Board.fromRow(row)).toList(); + + for (Board board in boards) { + List labels = await client.boardLabel.getBoardLabels(board); + board.boardLabels = labels; + } + + return boards; + } + + Stream> watchBoardsByWorkspace({required String workspaceId}) { + return client.getDBExecutor().watch(''' + SELECT * FROM board WHERE workspaceId = ? + ''', parameters: [workspaceId]).asyncMap((event) async { + List boards = event.map((row) => Board.fromRow(row)).toList(); + + for (Board board in boards) { + List labels = await client.boardLabel.getBoardLabels(board); + board.boardLabels = labels; + } + + return boards; + }); + } + + Future updateWorkspace(Workspace workspace) async { + await client.getDBExecutor().execute(''' + UPDATE workspace + set userId = ?1, name = ?2, description = ?3, visibility = ?4 + WHERE id = ?5 + ''', [ + workspace.userId, + workspace.name, + workspace.description, + workspace.visibility, + workspace.id + ]); + return true; + } + + Future deleteWorkspace(Workspace workspace) async { + await client + .getDBExecutor() + .execute('DELETE FROM workspace WHERE id = ?', [workspace.id]); + return true; + } +} + +class _BoardLabelRepository extends _Repository { + _BoardLabelRepository(super.client); + + Future createBoardLabel(BoardLabel boardLabel) async { + final results = await client.getDBExecutor().execute('''INSERT INTO + board_label(id, boardId, workspaceId, title, color, dateCreated) + VALUES(?, ?, ?, ?, ?, datetime()) + RETURNING *''', [ + boardLabel.id, + boardLabel.boardId, + boardLabel.workspaceId, + boardLabel.title, + boardLabel.color + ]); + if (results.isEmpty) { + throw Exception("Failed to add BoardLabel"); + } else { + return BoardLabel.fromRow(results.first); + } + } + + Future updateBoardLabel(BoardLabel boardLabel) async { + await client.getDBExecutor().execute(''' + UPDATE board_label + set title = ?1 + WHERE id = ?2 + ''', [boardLabel.title, boardLabel.id]); + return true; + } + + Future> getBoardLabels(Board board) async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM board_label WHERE boardId = ? ORDER BY dateCreated DESC + ''', [board.id]); + return results.map((row) => BoardLabel.fromRow(row)).toList(); + } +} + +class _CardLabelRepository extends _Repository { + _CardLabelRepository(super.client); + + Future createCardLabel(CardLabel cardLabel) async { + final results = await client.getDBExecutor().execute('''INSERT INTO + card_label(id, cardId, boardId, workspaceId, boardLabelId, dateCreated) + VALUES(?, ?, ?, ?, ?, datetime()) + RETURNING *''', [ + cardLabel.id, + cardLabel.cardId, + cardLabel.boardId, + cardLabel.workspaceId, + cardLabel.boardLabelId, + ]); + if (results.isEmpty) { + throw Exception("Failed to add CardLabel"); + } else { + return CardLabel.fromRow(results.first); + } + } + + Future deleteCardLabel(BoardLabel boardLabel) async { + await client.getDBExecutor().execute( + 'DELETE FROM card_label WHERE boardLabelId = ?', [boardLabel.id]); + return true; + } + + Future> getCardLabels(Cardlist card) async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM card_label WHERE cardId = ? ORDER BY dateCreated DESC + ''', [card.id]); + return results.map((row) => CardLabel.fromRow(row)).toList(); + } +} + +class DataClient { + late final _ActivityRepository activity; + late final _AttachmentRepository attachment; + late final _BoardRepository board; + late final _CardlistRepository card; + late final _CheckListRepository checklist; + late final _CommentRepository comment; + late final _ListboardRepository listboard; + late final _MemberRepository member; + late final _UserRepository user; + late final _WorkspaceRepository workspace; + late final _BoardLabelRepository boardLabel; + late final _CardLabelRepository cardLabel; + + late PowerSyncClient _powerSyncClient; + + DataClient() { + activity = _ActivityRepository(this); + attachment = _AttachmentRepository(this); + board = _BoardRepository(this); + card = _CardlistRepository(this); + checklist = _CheckListRepository(this); + comment = _CommentRepository(this); + listboard = _ListboardRepository(this); + member = _MemberRepository(this); + user = _UserRepository(this); + workspace = _WorkspaceRepository(this); + boardLabel = _BoardLabelRepository(this); + cardLabel = _CardLabelRepository(this); + } + + Future initialize() async { + _powerSyncClient = PowerSyncClient(); + await _powerSyncClient.initialize(); + } + + PowerSyncDatabase getDBExecutor() { + return _powerSyncClient.getDBExecutor(); + } + + bool isLoggedIn() { + return _powerSyncClient.isLoggedIn(); + } + + String? getUserId() { + return _powerSyncClient.getUserId(); + } + + Future getLoggedInUser() async { + String? userId = _powerSyncClient.getUserId(); + if (userId != null) { + return user.getUserById(userId: userId); + } else { + return null; + } + } + + Future logOut() async { + await _powerSyncClient.logout(); + } + + Future loginWithEmail(String email, String password) async { + String userId = await _powerSyncClient.loginWithEmail(email, password); + + TrelloUser? storedUser = await user.getUserById(userId: userId); + storedUser ??= await user.createUser(TrelloUser( + id: userId, + name: email.split('@')[0], + email: email, + password: password)); + return storedUser; + } + + Future signupWithEmail( + String name, String email, String password) async { + TrelloUser? storedUser = await user.checkUserExists(email); + if (storedUser != null) { + throw Exception('User for email already exists. Use Login instead.'); + } + return _powerSyncClient.signupWithEmail(name, email, password); + } + + SyncStatus getCurrentSyncStatus() { + return _powerSyncClient.currentStatus; + } + + Stream getStatusStream() { + return _powerSyncClient.statusStream; + } + + Future switchToOfflineMode() async { + await _powerSyncClient.switchToOfflineMode(); + } + + Future switchToOnlineMode() async { + await _powerSyncClient.switchToOnlineMode(); + } +} diff --git a/demos/supabase-trello/lib/protocol/powersync.dart b/demos/supabase-trello/lib/protocol/powersync.dart new file mode 100644 index 00000000..af7d0a56 --- /dev/null +++ b/demos/supabase-trello/lib/protocol/powersync.dart @@ -0,0 +1,269 @@ +// This file performs setup of the PowerSync database +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:powersync/powersync.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:trelloappclone_flutter/models/user.dart'; + +import '../models/schema.dart'; + +final log = Logger('powersync-supabase'); + +/// Postgres Response codes that we cannot recover from by retrying. +final List fatalResponseCodes = [ + // Class 22 — Data Exception + // Examples include data type mismatch. + RegExp(r'^22...$'), + // Class 23 — Integrity Constraint Violation. + // Examples include NOT NULL, FOREIGN KEY and UNIQUE violations. + RegExp(r'^23...$'), + // INSUFFICIENT PRIVILEGE - typically a row-level security violation + RegExp(r'^42501$'), +]; + +/// Use Supabase for authentication and data upload. +class SupabaseConnector extends PowerSyncBackendConnector { + PowerSyncDatabase db; + + Future? _refreshFuture; + + SupabaseConnector(this.db); + + /// Get a Supabase token to authenticate against the PowerSync instance. + @override + Future fetchCredentials() async { + // Wait for pending session refresh if any + await _refreshFuture; + + // Use Supabase token for PowerSync + final existingSession = Supabase.instance.client.auth.currentSession; + if (existingSession?.accessToken == null) { + // Not logged in + return null; + } + + // Force session refresh. + final authResponse = await Supabase.instance.client.auth.refreshSession(); + final session = authResponse.session; + if (session == null) { + // Probably shouldn't happen + return null; + } + + // Use the access token to authenticate against PowerSync + final token = session.accessToken; + + // userId and expiresAt are for debugging purposes only + final userId = session.user.id; + final expiresAt = session.expiresAt == null + ? null + : DateTime.fromMillisecondsSinceEpoch(session.expiresAt! * 1000); + return PowerSyncCredentials( + endpoint: dotenv.env['POWERSYNC_URL']!, + token: token, + userId: userId, + expiresAt: expiresAt); + } + + @override + void invalidateCredentials() { + // Trigger a session refresh if auth fails on PowerSync. + // Generally, sessions should be refreshed automatically by Supabase. + // However, in some cases it can be a while before the session refresh is + // retried. We attempt to trigger the refresh as soon as we get an auth + // failure on PowerSync. + // + // This could happen if the device was offline for a while and the session + // expired, and nothing else attempt to use the session it in the meantime. + // + // Timeout the refresh call to avoid waiting for long retries, + // and ignore any errors. Errors will surface as expired tokens. + _refreshFuture = Supabase.instance.client.auth + .refreshSession() + .timeout(const Duration(seconds: 5)) + .then((response) => null, onError: (error) => null); + } + + // Upload pending changes to Supabase. + @override + Future uploadData(PowerSyncDatabase database) async { + // This function is called whenever there is data to upload, whether the + // device is online or offline. + // If this call throws an error, it is retried periodically. + final transaction = await database.getNextCrudTransaction(); + if (transaction == null) { + return; + } + + final rest = Supabase.instance.client.rest; + CrudEntry? lastOp; + try { + // Note: If transactional consistency is important, use database functions + // or edge functions to process the entire transaction in a single call. + for (var op in transaction.crud) { + lastOp = op; + + final table = rest.from(op.table); + if (op.op == UpdateType.put) { + var data = Map.of(op.opData!); + data['id'] = op.id; + await table.upsert(data); + } else if (op.op == UpdateType.patch) { + await table.update(op.opData!).eq('id', op.id); + } else if (op.op == UpdateType.delete) { + await table.delete().eq('id', op.id); + } + } + + // All operations successful. + await transaction.complete(); + } on PostgrestException catch (e) { + if (e.code != null && + fatalResponseCodes.any((re) => re.hasMatch(e.code!))) { + /// Instead of blocking the queue with these errors, + /// discard the (rest of the) transaction. + /// + /// Note that these errors typically indicate a bug in the application. + /// If protecting against data loss is important, save the failing records + /// elsewhere instead of discarding, and/or notify the user. + log.severe('Data upload error - discarding $lastOp', e); + await transaction.complete(); + } else { + // Error may be retryable - e.g. network error or temporary server error. + // Throwing an error here causes this call to be retried after a delay. + rethrow; + } + } + } +} + +class PowerSyncClient { + PowerSyncClient._(); + + static final PowerSyncClient _instance = PowerSyncClient._(); + + bool _isInitialized = false; + bool _offlineMode = false; + + late final PowerSyncDatabase _db; + + factory PowerSyncClient() { + return _instance; + } + + Future initialize({bool offlineMode = false}) async { + if (!_isInitialized) { + _offlineMode = offlineMode; + await dotenv.load(fileName: '.env'); + await _openDatabase(); + _isInitialized = true; + } + } + + PowerSyncDatabase getDBExecutor() { + if (!_isInitialized) { + throw Exception( + 'PowerSyncDatabase not initialized. Call initialize() first.'); + } + return _db; + } + + bool isLoggedIn() { + return Supabase.instance.client.auth.currentSession?.accessToken != null; + } + + /// id of the user currently logged in + String? getUserId() { + return Supabase.instance.client.auth.currentSession?.user.id; + } + + Future getDatabasePath() async { + final dir = await getApplicationSupportDirectory(); + return join(dir.path, 'powersync-trello-demo.db'); + } + + _loadSupabase() async { + await Supabase.initialize( + url: dotenv.env['SUPABASE_URL']!, + anonKey: dotenv.env['SUPABASE_ANON_KEY']!, + ); + } + + Future _openDatabase() async { + // Open the local database + _db = PowerSyncDatabase(schema: schema, path: await getDatabasePath()); + await _db.initialize(); + + await _loadSupabase(); + + if (isLoggedIn() && !_offlineMode) { + // If the user is already logged in, connect immediately. + // Otherwise, connect once logged in. + _db.connect(connector: SupabaseConnector(_db)); + } + + Supabase.instance.client.auth.onAuthStateChange.listen((data) async { + final AuthChangeEvent event = data.event; + if (event == AuthChangeEvent.signedIn) { + // Connect to PowerSync when the user is signed in + _db.connect(connector: SupabaseConnector(_db)); + } else if (event == AuthChangeEvent.signedOut) { + // Implicit sign out - disconnect, but don't delete data + await _db.disconnect(); + } + }); + } + + bool isOfflineMode() { + return _offlineMode; + } + + Future switchToOfflineMode() async { + log.info('Switching to Offline mode'); + _offlineMode = true; + await _db.disconnect(); + } + + Future switchToOnlineMode() async { + log.info('Switching to Online mode'); + _offlineMode = false; + if (isLoggedIn()) { + _db.connect(connector: SupabaseConnector(_db)); + } + } + + Future signupWithEmail( + String name, String email, String password) async { + AuthResponse authResponse = await Supabase.instance.client.auth + .signUp(email: email, password: password); + + return TrelloUser( + id: authResponse.user!.id, + name: name, + email: email, + password: password); + } + + Future loginWithEmail(String email, String password) async { + AuthResponse authResponse = await Supabase.instance.client.auth + .signInWithPassword(email: email, password: password); + + return authResponse.user!.id; + } + + /// Explicit sign out - clear database and log out. + Future logout() async { + if (!_isInitialized) { + throw Exception( + 'PowerSyncClient not initialized. Call initialize() first.'); + } + await Supabase.instance.client.auth.signOut(); + await _db.disconnectAndClear(); + } + + /// Getting current connection status + SyncStatus get currentStatus => _db.currentStatus; + Stream get statusStream => _db.statusStream; +} diff --git a/demos/supabase-trello/lib/utils/color.dart b/demos/supabase-trello/lib/utils/color.dart new file mode 100644 index 00000000..d4ea5f21 --- /dev/null +++ b/demos/supabase-trello/lib/utils/color.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; + +const brandColor = Color(0xff38b6ff); +const whiteShade = Color(0xfff0f0f0); +const dangerColor = Color(0xff800200); +const themeColor = Color(0xff02457a); diff --git a/demos/supabase-trello/lib/utils/config.dart b/demos/supabase-trello/lib/utils/config.dart new file mode 100644 index 00000000..7b10fea7 --- /dev/null +++ b/demos/supabase-trello/lib/utils/config.dart @@ -0,0 +1,44 @@ +import 'package:flutter/painting.dart'; + +const logo = "assets/trello-logo.png"; +const landingImage = "assets/landing.jpg"; +const backgrounds = [ + "#ADD8E6" + "#89CFF0", + "#0000FF", + "#7393B3", + "#088F8F", + "#0096FF", + "#5F9EA0", + "#0047AB", + "#6495ED", + "#00FFFF", + "#00008B", + "#6F8FAF", + "#1434A4", + "#7DF9FF", + "#6082B6", + "#00A36C", + "#3F00FF", + "#5D3FD3" +]; + +const listMenu = [ + "Add Card", + "Copy list", + "Move list", + "Watch", + "Sort by", + "Move all cards in this list", + "Archive all cards in this list", + "Archive list" +]; + +const labels = [ + Color(0xffADD8E6), + Color(0xff89CFF0), + Color(0xff0000FF), + Color(0xff7393B3) +]; + +enum Sign { signUp, logIn } diff --git a/demos/supabase-trello/lib/utils/constant.dart b/demos/supabase-trello/lib/utils/constant.dart new file mode 100644 index 00000000..2a1a9bd0 --- /dev/null +++ b/demos/supabase-trello/lib/utils/constant.dart @@ -0,0 +1,40 @@ +const headline = "Move teamwork forward - even on the go"; +const terms = + "By signing up, you agree to our Terms of service and Privacy Policy"; +const contact = "Contact support"; +const visibilityConfigurations = [ + { + "type": "Private", + "description": + "The board is private. Only people added to the board can view and edit it" + }, + {"type": "Workspace", "description": "Anyone at BOARDNAME can see this file"}, + { + "type": "Public", + "description": + "The board is public. It's visible to anyone with the link and will show up in search engines like Google. Only people added to the board can edit it" + }, +]; +const defaultDescription = + "It's your board's time to shine! Let people know what this board is used for and what they can expect to see"; +const powerups = [ + { + "title": "Calendar Power-Up", + "description": + "See your work over time! The Trello Calendar Power-Up displays all cards with due dates by month" + }, + { + "title": "Card Aging", + "description": + "Quickly visualize inactive cards on your board, and keep tasks from going incomplete" + }, + { + "title": "Voting", + "description": "Give power to the people, and allow users to vote on cards" + }, + { + "title": "List limits", + "description": + "Set a limit on your lists and we'll highlight the list if the number of cards in it passes the limit" + } +]; diff --git a/demos/supabase-trello/lib/utils/data_generator.dart b/demos/supabase-trello/lib/utils/data_generator.dart new file mode 100644 index 00000000..e85c2cdb --- /dev/null +++ b/demos/supabase-trello/lib/utils/data_generator.dart @@ -0,0 +1,132 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:random_name_generator/random_name_generator.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; +import 'package:trelloappclone_flutter/utils/config.dart'; +import 'package:trelloappclone_flutter/utils/service.dart'; +import 'package:trelloappclone_flutter/utils/trello_provider.dart'; +import 'package:trelloappclone_flutter/models/listboard.dart'; +import 'package:trelloappclone_flutter/models/board.dart'; +import 'package:trelloappclone_flutter/models/workspace.dart'; +import 'package:trelloappclone_flutter/models/card.dart'; +import 'package:trelloappclone_flutter/models/checklist.dart'; +import 'package:trelloappclone_flutter/models/comment.dart'; + +/// This class generates an example workspace with some boards, cards, etc for each +class DataGenerator with Service { + Random random = Random(); + var randomNames = RandomNames(Zone.us); + + List _generateBoardNames(String workspaceName) { + String prepend = workspaceName.splitMapJoin(' ', + onMatch: (match) => '', + onNonMatch: (text) => text.substring(0, 1).toUpperCase()); + return [ + '$prepend MVP App', + '$prepend User Auth Service', + '$prepend Transactions Service', + '$prepend Reporting Service', + '$prepend DevOps' + ]; + } + + List _generateListNames() { + return ['To Do', 'In Progress', 'To Test', 'Testing', 'To Release', 'Done']; + } + + List _generateCardNames(String prepend, int nrOfCards) { + return List.generate(nrOfCards, (index) => '$prepend Card $index'); + } + + createSampleWorkspace( + String workspaceName, TrelloProvider trello, BuildContext context) async { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 3), + margin: EdgeInsets.only( + bottom: MediaQuery.of(context).size.height * 0.1, // 10% from bottom + right: 20, + left: 20, + ), + content: const Row( + children: [ + Icon(Icons.sync, color: brandColor), + SizedBox(width: 12), + Text('Generating Workspace Data...'), + ], + ), + behavior: SnackBarBehavior.floating, + ), + ); + + Workspace workspace = await createWorkspace(context, + name: workspaceName, + description: 'Example workspace', + visibility: 'Public'); + for (String boardName in _generateBoardNames(workspaceName)) { + //create board + Board newBoard = Board( + id: randomUuid(), + workspaceId: workspace.id, + userId: trello.user.id, + name: boardName, + visibility: 'Workspace', + background: backgrounds[random.nextInt(16)]); + await createBoard(context, newBoard); + + int listIndex = 1; + //create lists for each board + for (String listName in _generateListNames()) { + Listboard newList = Listboard( + id: randomUuid(), + workspaceId: workspace.id, + boardId: newBoard.id, + userId: trello.user.id, + name: listName, + order: listIndex++); + await addList(newList); + + //create cards per list + int cardIndex = 1; + for (String cardName + in _generateCardNames(listName, random.nextInt(10))) { + Cardlist newCard = Cardlist( + id: randomUuid(), + workspaceId: workspace.id, + listId: newList.id, + userId: trello.user.id, + name: cardName, + rank: cardIndex++); + await addCard(newCard); + await createComment(Comment( + id: randomUuid(), + workspaceId: workspace.id, + cardId: newCard.id, + userId: trello.user.id, + description: '${randomNames.name()} said something interesting', + )); + await createChecklist(Checklist( + id: randomUuid(), + workspaceId: workspace.id, + cardId: newCard.id, + name: '${randomNames.name()} need to check this', + status: false)); + await createChecklist(Checklist( + id: randomUuid(), + workspaceId: workspace.id, + cardId: newCard.id, + name: '${randomNames.name()} need to check this', + status: false)); + await createActivity( + workspaceId: workspace.id, + boardId: newBoard.id, + card: newCard.id, + description: '${randomNames.name()} updated this card'); + } + } + } + } +} diff --git a/demos/supabase-trello/lib/utils/service.dart b/demos/supabase-trello/lib/utils/service.dart new file mode 100644 index 00000000..a1f46a6a --- /dev/null +++ b/demos/supabase-trello/lib/utils/service.dart @@ -0,0 +1,589 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:developer'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart' hide Card; +import 'package:trelloappclone_flutter/features/home/presentation/custom_search.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; +import 'package:trelloappclone_flutter/models/listboard.dart'; +import 'package:trelloappclone_flutter/models/board.dart'; +import 'package:trelloappclone_flutter/models/workspace.dart'; +import 'package:trelloappclone_flutter/models/user.dart'; +import 'package:trelloappclone_flutter/models/card.dart'; +import 'package:trelloappclone_flutter/models/member.dart'; +import 'package:trelloappclone_flutter/models/checklist.dart'; +import 'package:trelloappclone_flutter/models/comment.dart'; +import 'package:trelloappclone_flutter/models/activity.dart'; +import 'package:trelloappclone_flutter/models/board_label.dart'; +import 'package:trelloappclone_flutter/models/card_label.dart'; +import 'package:trelloappclone_flutter/models/attachment.dart'; + +// ignore: depend_on_referenced_packages +import 'package:uuid/uuid.dart'; + +import '../main.dart'; + +var uuid = const Uuid(); + +mixin Service { + randomUuid() { + return uuid.v4(); + } + + //sign up new user + signUp( + {required String name, + required String email, + required String password, + required BuildContext context}) async { + try { + TrelloUser user = await dataClient.signupWithEmail(name, email, password); + await dataClient.user.createUser(user); + + if (context.mounted) { + Navigator.pushNamed(context, '/'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 3), + margin: EdgeInsets.only( + bottom: + MediaQuery.of(context).size.height * 0.1, // 10% from bottom + right: 20, + left: 20, + ), + content: const Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Icon(Icons.check, color: brandColor), + SizedBox(width: 12), + Text('Account Created'), + ], + ), + SizedBox(height: 4), + Text( + 'Log in with your new credentials', + softWrap: true, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ], + ), + behavior: SnackBarBehavior.floating, + ), + ); + } + } on Exception catch (e) { + log('Error with signup: $e', error: e); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 5), + margin: EdgeInsets.only( + bottom: MediaQuery.of(context).size.height * 0.1, // 10% from bottom + right: 20, + left: 20, + ), + backgroundColor: Colors.red[100], + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Row( + children: [ + Icon(Icons.error, color: Colors.red), + SizedBox(width: 12), + Text('Sign Up Error'), + ], + ), + const SizedBox(height: 4), + Text( + e.toString(), + softWrap: true, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ], + ), + behavior: SnackBarBehavior.floating, + ), + ); + } + } + + //log in existing user + logIn(String email, String password, BuildContext context) async { + try { + TrelloUser user = await dataClient.loginWithEmail(email, password); + trello.setUser(user); + + if (context.mounted) { + Navigator.pushNamed(context, '/home'); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + duration: Duration(seconds: 5), + content: Row( + children: [ + Icon(Icons.sync, color: brandColor), + SizedBox(width: 12), + Text('Syncing Workspaces...'), + ], + ), + behavior: SnackBarBehavior.floating, + ), + ); + } + } on Exception catch (e) { + log('Error with login: $e', error: e); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 5), + margin: EdgeInsets.only( + bottom: MediaQuery.of(context).size.height * 0.1, // 10% from bottom + right: 20, + left: 20, + ), + backgroundColor: Colors.red[100], + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Row( + children: [ + Icon(Icons.error, color: Colors.red), + SizedBox(width: 12), + Text('Login Error'), + ], + ), + const SizedBox(height: 4), + Text( + e.toString(), + softWrap: true, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ], + ), + behavior: SnackBarBehavior.floating, + ), + ); + } + } + + //log out user + logOut(BuildContext context) async { + try { + await dataClient.logOut(); + } on Exception catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 5), + margin: EdgeInsets.only( + bottom: MediaQuery.of(context).size.height * 0.1, // 10% from bottom + right: 20, + left: 20, + ), + backgroundColor: Colors.red[100], + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Row( + children: [ + Icon(Icons.error, color: Colors.red), + SizedBox(width: 12), + Text('Logout Error'), + ], + ), + const SizedBox(height: 4), + Text( + e.toString(), + softWrap: true, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ], + ), + behavior: SnackBarBehavior.floating, + ), + ); + } + } + + switchToOfflineMode() async { + dataClient.switchToOfflineMode(); + } + + switchToOnlineMode() async { + dataClient.switchToOnlineMode(); + } + + //search for a board + search(BuildContext context) async { + List allboards = await dataClient.board.getAllBoards(); + + if (context.mounted) { + showSearch(context: context, delegate: CustomSearchDelegate(allboards)); + } + } + + //create workspace + createWorkspace(BuildContext context, + {required String name, + required String description, + required String visibility}) async { + Workspace workspace = Workspace( + id: randomUuid(), + userId: trello.user.id, + name: name, + description: description, + visibility: visibility); + + try { + Workspace addedWorkspace = + await dataClient.workspace.createWorkspace(workspace); + + Member newMember = Member( + id: randomUuid(), + workspaceId: addedWorkspace.id, + userId: trello.user.id, + name: trello.user.name ?? trello.user.email, + role: "Admin"); + + await dataClient.member.addMember(newMember); + + if (context.mounted) { + Navigator.pushNamed(context, "/home"); + } + + return addedWorkspace; + } on Exception catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 5), + margin: EdgeInsets.only( + bottom: MediaQuery.of(context).size.height * 0.1, // 10% from bottom + right: 20, + left: 20, + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Row( + children: [ + Icon(Icons.check, color: brandColor), + SizedBox(width: 12), + Text('Trello Clone'), + ], + ), + const SizedBox(height: 4), + Text(e.toString()), + ], + ), + behavior: SnackBarBehavior.floating, + ), + ); + } + } + + //get workspaces of a specific user using user ID + Future> getWorkspaces() async { + List workspaces = + await dataClient.workspace.getWorkspacesByUser(userId: trello.user.id); + trello.setWorkspaces(workspaces); + return workspaces; + } + + //get a stream of workspaces for user, so we can react on distributed changes to it + Stream> getWorkspacesStream() { + return dataClient.workspace + .watchWorkspacesByUser(userId: trello.user.id) + .map((workspaces) { + trello.setWorkspaces(workspaces); + return workspaces; + }); + } + + //create board + createBoard(BuildContext context, Board brd) async { + try { + var labelColors = [{}]; + labelColors = [ + {"color": "fd6267", "name": "Bug"}, + {"color": "67ddf3", "name": "Feature"}, + {"color": "a282ff", "name": "Enhancement"}, + {"color": "f7b94a", "name": "Documentation"}, + {"color": "f8ff6e", "name": "Marketing"} + ]; + + await dataClient.board.createBoard(brd); + // Add the default labels to the board + for (var label in labelColors) { + await dataClient.boardLabel.createBoardLabel(BoardLabel( + id: randomUuid(), + boardId: brd.id, + workspaceId: brd.workspaceId, + title: label["name"] ?? "", + color: label["color"] ?? "", + dateCreated: DateTime.now())); + } + + if (context.mounted) { + Navigator.pushNamed(context, "/home"); + } + } on Exception catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 5), + margin: EdgeInsets.only( + bottom: MediaQuery.of(context).size.height * 0.1, // 10% from bottom + right: 20, + left: 20, + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Row( + children: [ + Icon(Icons.check, color: brandColor), + SizedBox(width: 12), + Text('Trello Clone'), + ], + ), + const SizedBox(height: 4), + Text(e.toString()), + ], + ), + behavior: SnackBarBehavior.floating, + ), + ); + } + } + + //get boards of a specific workspace by Workspace ID + Future> getBoards(String workspaceId) async { + List boards = await dataClient.workspace + .getBoardsByWorkspace(workspaceId: workspaceId); + trello.setBoards(boards); + return boards; + } + + //watch boards of a specific workspace by Workspace ID via a stream + Stream> getBoardsStream(String workspaceId) { + return dataClient.workspace + .watchBoardsByWorkspace(workspaceId: workspaceId) + .map((boards) { + trello.setBoards(boards); + return boards; + }); + } + + //update workspace + Future updateWorkspace(Workspace wkspc) async { + return await dataClient.workspace.updateWorkspace(wkspc); + } + + //get user by Id + Future getUserById(String userId) async { + TrelloUser? user = await dataClient.user.getUserById(userId: userId); + return user; + } + + //get information of members + Future> getMembersInformation(List mmbrs) async { + List usrs = + await dataClient.member.getInformationOfMembers(mmbrs); + return usrs; + } + + Future inviteUserToWorkspace(String email, Workspace workspace) async { + TrelloUser? user = await dataClient.user.checkUserExists(email); + if (user != null) { + Member member = Member( + id: randomUuid(), + workspaceId: workspace.id, + userId: user.id, + name: user.name ?? user.email, + role: "Admin"); + await dataClient.member.addMember(member); + workspace.members?.add(member); + return true; + } + return false; + } + + //remove Member from Workspace + Future removeMemberFromWorkspace( + Member mmbr, Workspace wkspc) async { + Workspace updatedWorkspace = + await dataClient.member.deleteMember(mmbr, wkspc); + return updatedWorkspace; + } + + //update offline status + Future updateOfflineStatus(Board brd) async { + return await dataClient.board.updateBoard(brd); + } + + //get lists by board + Future> getListsByBoard(Board brd) async { + List brdlist = + await dataClient.listboard.getListsByBoard(boardId: brd.id); + trello.setListBoard(brdlist); + return brdlist; + } + + //watch lists by board via Stream + Stream> getListsByBoardStream(Board brd) { + return dataClient.listboard.watchListsByBoard(boardId: brd.id).map((lists) { + trello.setListBoard(lists); + return lists; + }); + } + + //add list + Future addList(Listboard lst) async { + await dataClient.listboard.createList(lst); + createActivity( + workspaceId: lst.workspaceId, + description: "${trello.user.name} added a new list ${lst.name}"); + } + + //add list + Future updateListOrder(String listId, int newOrder) async { + await dataClient.listboard.updateListOrder(listId, newOrder); + } + + //add card + Future addCard(Cardlist crd) async { + Cardlist newcrd = await dataClient.card.createCard(crd); + createActivity( + card: newcrd.id, + workspaceId: newcrd.workspaceId, + description: "${trello.user.name} added a new card ${crd.name}"); + } + + //update card + Future updateCard(Cardlist crd) async { + await dataClient.card.updateCard(crd); + + createActivity( + card: crd.id, + workspaceId: crd.workspaceId, + description: "${trello.user.name} updated the card ${crd.name}"); + } + + //delete card + Future deleteCard(Cardlist crd) async { + await dataClient.card.deleteCard(crd); + + createActivity( + card: crd.id, + workspaceId: crd.workspaceId, + description: "${trello.user.name} deleted the card ${crd.name}"); + } + + Future archiveCardsInList(Listboard list) async { + return dataClient.listboard.archiveCardsInList(list); + } + + //add card + Future addCardLabel(CardLabel crdlbl, BoardLabel brdlbl) async { + final newCardLabel = await dataClient.cardLabel.createCardLabel(crdlbl); + createActivity( + card: crdlbl.cardId, + workspaceId: brdlbl.workspaceId, + description: "${trello.user.name} added a new label '${brdlbl.title}'"); + + return newCardLabel; + } + + //add card + Future deleteCardLabel(cardId, BoardLabel brdlbl) async { + await dataClient.cardLabel.deleteCardLabel(brdlbl); + createActivity( + card: cardId, + workspaceId: brdlbl.workspaceId, + description: "${trello.user.name} deleted the label '${brdlbl.title}'"); + } + + //updateBoardLabel + Future updateBoardLabel(BoardLabel brdlbl) async { + await dataClient.boardLabel.updateBoardLabel(brdlbl); + } + + //create activity + Future createActivity( + {required String workspaceId, + String? boardId, + required String description, + String? card}) async { + await dataClient.activity.createActivity(Activity( + id: randomUuid(), + workspaceId: workspaceId, + boardId: boardId, + userId: trello.user.id, + cardId: card, + description: description, + dateCreated: DateTime.now())); + } + + //get activities of a specific card + Future> getActivities(Cardlist crd) async { + return dataClient.activity.getActivities(crd); + } + + //create comment + Future createComment(Comment cmmt) async { + await dataClient.comment.createComment(cmmt); + } + + //create checklist + Future createChecklist(Checklist chcklst) async { + await dataClient.checklist.createChecklist(chcklst); + } + + Future> getChecklists(Cardlist crd) async { + List chcklsts = await dataClient.checklist.getChecklists(crd); + return chcklsts; + } + + Future updateChecklist(Checklist chcklst) async { + await dataClient.checklist.updateChecklist(chcklst); + } + + Future deleteChecklist(Cardlist crd) async { + await dataClient.checklist.deleteChecklist(crd); + } + + Future uploadFile(Cardlist crd) async { + FilePickerResult? result = + await FilePicker.platform.pickFiles(allowMultiple: false); + if (result != null) { + addAttachment(result.files[0].path ?? "", crd); + } + } + + Future addAttachment(String path, Cardlist crd) async { + //TODO: fix uploads + // var uploadDescription = await client.attachment.getUploadDescription(path); + bool success = false; + // if (uploadDescription != null) { + // var uploader = FileUploader(uploadDescription); + // await uploader.upload( + // File(path).readAsBytes().asStream(), File(path).lengthSync()); + // success = await client.attachment.verifyUpload(path); + // } + // if (success) { + // insertAttachment(crd, path); + // } + return success; + } + + Future insertAttachment(Cardlist crd, String path) async { + await dataClient.attachment.addAttachment(Attachment( + id: randomUuid(), + workspaceId: crd.workspaceId, + userId: trello.user.id, + cardId: crd.id, + attachment: path)); + } +} diff --git a/demos/supabase-trello/lib/utils/trello_provider.dart b/demos/supabase-trello/lib/utils/trello_provider.dart new file mode 100644 index 00000000..16830e39 --- /dev/null +++ b/demos/supabase-trello/lib/utils/trello_provider.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/models/listboard.dart'; +import 'package:trelloappclone_flutter/models/board.dart'; +import 'package:trelloappclone_flutter/models/workspace.dart'; +import 'package:trelloappclone_flutter/models/user.dart'; +import 'package:trelloappclone_flutter/models/card.dart'; + +import 'config.dart'; + +class TrelloProvider extends ChangeNotifier { + late TrelloUser _user; + TrelloUser get user => _user; + + List _workspaces = []; + List get workspaces => _workspaces; + + List _boards = []; + List get boards => _boards; + + String _selectedBackground = backgrounds[0]; + String get selectedBackground => _selectedBackground; + + List _lstbrd = []; + List get lstbrd => _lstbrd; + + late Board _selectedBoard; + Board get selectedBoard => _selectedBoard; + + late Workspace _selectedWorkspace; + Workspace get selectedWorkspace => _selectedWorkspace; + + Cardlist? _selectedCard; + Cardlist? get selectedCard => _selectedCard; + + void setUser(TrelloUser user) { + _user = user; + notifyListeners(); + } + + void setWorkspaces(List wkspcs) { + _workspaces = wkspcs; + notifyListeners(); + } + + void setBoards(List brd) { + _boards = brd; + notifyListeners(); + } + + void setSelectedBg(String slctbg) { + _selectedBackground = slctbg; + notifyListeners(); + } + + void setListBoard(List lstbrd) { + _lstbrd = lstbrd; + notifyListeners(); + } + + void setSelectedBoard(Board brd) { + _selectedBoard = brd; + notifyListeners(); + } + + void setSelectedWorkspace(Workspace workspace) { + _selectedWorkspace = workspace; + notifyListeners(); + } + + void setSelectedCard(Cardlist? card) { + _selectedCard = card; + notifyListeners(); + } +} diff --git a/demos/supabase-trello/lib/utils/widgets.dart b/demos/supabase-trello/lib/utils/widgets.dart new file mode 100644 index 00000000..bfebae06 --- /dev/null +++ b/demos/supabase-trello/lib/utils/widgets.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; + +class LabelDiplay extends StatefulWidget { + final String label; + final String color; + const LabelDiplay({required this.label, required this.color, super.key}); + + @override + State createState() => _LabelDiplayState(); +} + +class _LabelDiplayState extends State { + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + color: Color(int.parse(widget.color, radix: 16) + 0xFF000000), + borderRadius: const BorderRadius.all(Radius.circular(5.0))), + child: Center( + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Text( + widget.label, + style: TextStyle( + color: ((int.parse(widget.color, radix: 16)) > 186 + ? Colors.black + : Colors.white)), + ), + ), + )); + } +} + +class ColorSquare extends StatefulWidget { + final String bckgrd; + const ColorSquare({required this.bckgrd, super.key}); + + @override + State createState() => _ColorSquareState(); +} + +class _ColorSquareState extends State { + @override + Widget build(BuildContext context) { + return Container( + height: 40.0, + width: 40.0, + decoration: BoxDecoration( + color: Color( + int.parse(widget.bckgrd.substring(1, 7), radix: 16) + 0xFF000000), + borderRadius: const BorderRadius.all(Radius.circular(5.0))), + ); + } +} + +class BlueRectangle extends StatefulWidget { + const BlueRectangle({super.key}); + + @override + State createState() => _BlueRectangleState(); +} + +class _BlueRectangleState extends State { + @override + Widget build(BuildContext context) { + return Container( + height: 100.0, + width: 300.0, + decoration: const BoxDecoration( + color: brandColor, + borderRadius: BorderRadius.all(Radius.circular(5.0))), + ); + } +} diff --git a/demos/supabase-trello/lib/widgets/thirdparty/README.md b/demos/supabase-trello/lib/widgets/thirdparty/README.md new file mode 100644 index 00000000..c1a4c771 --- /dev/null +++ b/demos/supabase-trello/lib/widgets/thirdparty/README.md @@ -0,0 +1,5 @@ +# BoardView + +The code for this BoardView widget comes from this repo: https://github.com/jakebonk/FlutterBoardView + +However, since that project is not being maintained anymore, and somehow the latest code in the `master` repo does not match the actual lib code from pub.dev, we pulled in the code directly in this project in order to make it compilable again with latest Flutter version. \ No newline at end of file diff --git a/demos/supabase-trello/lib/widgets/thirdparty/board_item.dart b/demos/supabase-trello/lib/widgets/thirdparty/board_item.dart new file mode 100644 index 00000000..4b660d33 --- /dev/null +++ b/demos/supabase-trello/lib/widgets/thirdparty/board_item.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +import 'board_list.dart'; + +typedef OnDropItem = void Function(int? listIndex, int? itemIndex, + int? oldListIndex, int? oldItemIndex, BoardItemState state); +typedef OnTapItem = void Function( + int listIndex, int itemIndex, BoardItemState state); +typedef OnStartDragItem = void Function( + int? listIndex, int? itemIndex, BoardItemState state); +typedef OnDragItem = void Function(int oldListIndex, int oldItemIndex, + int newListIndex, int newItemIndex, BoardItemState state); + +class BoardItem extends StatefulWidget { + final BoardListState? boardList; + final Widget? item; + final int? index; + final OnDropItem? onDropItem; + final OnTapItem? onTapItem; + final OnStartDragItem? onStartDragItem; + final OnDragItem? onDragItem; + final bool draggable; + + const BoardItem( + {super.key, + this.boardList, + this.item, + this.index, + this.onDropItem, + this.onTapItem, + this.onStartDragItem, + this.draggable = true, + this.onDragItem}); + + @override + State createState() { + return BoardItemState(); + } +} + +class BoardItemState extends State + with AutomaticKeepAliveClientMixin { + late double height; + double? width; + + @override + bool get wantKeepAlive => true; + + void onDropItem(int? listIndex, int? itemIndex) { + if (widget.onDropItem != null) { + // check if itemIndex == items.length + // log everything + widget.onDropItem!( + listIndex, + itemIndex, + widget.boardList!.widget.boardView!.startListIndex, + widget.boardList!.widget.boardView!.startItemIndex, + this); + } + widget.boardList!.widget.boardView!.draggedItemIndex = null; + widget.boardList!.widget.boardView!.draggedListIndex = null; + if (widget.boardList!.widget.boardView!.listStates[listIndex!].mounted) { + widget.boardList!.widget.boardView!.listStates[listIndex].setState(() {}); + } + } + + void _startDrag(Widget item, BuildContext context) { + if (widget.boardList!.widget.boardView != null) { + widget.boardList!.widget.boardView!.onDropItem = onDropItem; + if (widget.boardList!.mounted) { + widget.boardList!.setState(() {}); + } + widget.boardList!.widget.boardView!.draggedItemIndex = widget.index; + widget.boardList!.widget.boardView!.height = context.size!.height; + widget.boardList!.widget.boardView!.draggedListIndex = + widget.boardList!.widget.index; + widget.boardList!.widget.boardView!.startListIndex = + widget.boardList!.widget.index; + widget.boardList!.widget.boardView!.startItemIndex = widget.index; + widget.boardList!.widget.boardView!.draggedItem = item; + if (widget.onStartDragItem != null) { + widget.onStartDragItem!( + widget.boardList!.widget.index, widget.index, this); + } + widget.boardList!.widget.boardView!.run(); + if (widget.boardList!.widget.boardView!.mounted) { + widget.boardList!.widget.boardView!.setState(() {}); + } + } + } + + void afterFirstLayout(BuildContext context) { + height = context.size!.height; + width = context.size!.width; + } + + @override + Widget build(BuildContext context) { + super.build(context); + WidgetsBinding.instance + .addPostFrameCallback((_) => afterFirstLayout(context)); + if (widget.boardList!.itemStates.length > widget.index!) { + widget.boardList!.itemStates.removeAt(widget.index!); + } + widget.boardList!.itemStates.insert(widget.index!, this); + return GestureDetector( + onTapDown: (otd) { + if (widget.draggable) { + RenderBox object = context.findRenderObject() as RenderBox; + Offset pos = object.localToGlobal(Offset.zero); + RenderBox box = + widget.boardList!.context.findRenderObject() as RenderBox; + Offset listPos = box.localToGlobal(Offset.zero); + widget.boardList!.widget.boardView!.leftListX = listPos.dx; + widget.boardList!.widget.boardView!.topListY = listPos.dy; + widget.boardList!.widget.boardView!.topItemY = pos.dy; + widget.boardList!.widget.boardView!.bottomItemY = + pos.dy + object.size.height; + widget.boardList!.widget.boardView!.bottomListY = + listPos.dy + box.size.height; + widget.boardList!.widget.boardView!.rightListX = + listPos.dx + box.size.width; + + widget.boardList!.widget.boardView!.initialX = pos.dx; + widget.boardList!.widget.boardView!.initialY = pos.dy; + } + }, + onTapCancel: () {}, + onTap: () { + if (widget.onTapItem != null) { + if (widget.boardList != null && + widget.boardList!.widget.index != null && + widget.index != null) { + widget.onTapItem!( + widget.boardList!.widget.index!, widget.index!, this); + } else {} + } + }, + onLongPress: () { + if (!widget.boardList!.widget.boardView!.widget.isSelecting && + widget.draggable) { + _startDrag(widget, context); + } + }, + child: widget.item, + ); + } +} diff --git a/demos/supabase-trello/lib/widgets/thirdparty/board_list.dart b/demos/supabase-trello/lib/widgets/thirdparty/board_list.dart new file mode 100644 index 00000000..65a04692 --- /dev/null +++ b/demos/supabase-trello/lib/widgets/thirdparty/board_list.dart @@ -0,0 +1,192 @@ +import 'board_item.dart'; +import 'boardview.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +typedef OnDropList = void Function(int? listIndex, int? oldListIndex); +typedef OnTapList = void Function(int? listIndex); +typedef OnStartDragList = void Function(int? listIndex); + +class BoardList extends StatefulWidget { + final List? header; + final Widget? footer; + final List? items; + final Color? backgroundColor; + final Color? headerBackgroundColor; + final BoardViewState? boardView; + final OnDropList? onDropList; + final OnTapList? onTapList; + final OnStartDragList? onStartDragList; + final bool draggable; + + const BoardList({ + super.key, + this.header, + this.items, + this.footer, + this.backgroundColor, + this.headerBackgroundColor, + this.boardView, + this.draggable = true, + this.index, + this.onDropList, + this.onTapList, + this.onStartDragList, + }); + + final int? index; + + @override + State createState() { + return BoardListState(); + } +} + +class BoardListState extends State + with AutomaticKeepAliveClientMixin { + List itemStates = []; + ScrollController boardListController = ScrollController(); + + void onDropList(int? listIndex) { + if (widget.onDropList != null) { + widget.onDropList!(listIndex, widget.boardView!.startListIndex); + } + widget.boardView!.draggedListIndex = null; + if (widget.boardView!.mounted) { + widget.boardView!.setState(() {}); + } + } + + void _startDrag(Widget item, BuildContext context) { + if (widget.boardView != null && widget.draggable) { + if (widget.onStartDragList != null) { + widget.onStartDragList!(widget.index); + } + widget.boardView!.startListIndex = widget.index; + widget.boardView!.height = context.size!.height; + widget.boardView!.draggedListIndex = widget.index!; + widget.boardView!.draggedItemIndex = null; + widget.boardView!.draggedItem = item; + widget.boardView!.onDropList = onDropList; + widget.boardView!.run(); + if (widget.boardView!.mounted) { + widget.boardView!.setState(() {}); + } + } + } + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + List listWidgets = []; + if (widget.header != null) { + if (widget.headerBackgroundColor != null) { + // ignore: unused_local_variable + Color? headerBackgroundColor = const Color.fromARGB(255, 255, 255, 255); + headerBackgroundColor = widget.headerBackgroundColor; + } + listWidgets.add(GestureDetector( + onTap: () { + if (widget.onTapList != null) { + widget.onTapList!(widget.index); + } + }, + onTapDown: (otd) { + if (widget.draggable) { + RenderBox object = context.findRenderObject() as RenderBox; + Offset pos = object.localToGlobal(Offset.zero); + widget.boardView!.initialX = pos.dx; + widget.boardView!.initialY = pos.dy; + + widget.boardView!.rightListX = pos.dx + object.size.width; + widget.boardView!.leftListX = pos.dx; + } + }, + onTapCancel: () {}, + onLongPress: () { + if (!widget.boardView!.widget.isSelecting && widget.draggable) { + _startDrag(widget, context); + } + }, + child: Container( + color: widget.headerBackgroundColor, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: widget.header!), + ))); + } + if (widget.items != null) { + listWidgets.add(Flexible( + fit: FlexFit.loose, + child: ListView.builder( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + controller: boardListController, + itemCount: widget.items!.length, + itemBuilder: (ctx, index) { + // if index does not exist + if (widget.items!.length <= index) { + //set index to the last index + return Opacity( + opacity: 0.0, + child: widget.boardView!.draggedItem!, + ); + } + + if (widget.items![index].boardList == null || + widget.items![index].index != index || + widget.items![index].boardList!.widget.index != + widget.index || + widget.items![index].boardList != this) { + widget.items![index] = BoardItem( + boardList: this, + item: widget.items![index].item, + draggable: widget.items![index].draggable, + index: index, + onDropItem: widget.items![index].onDropItem, + onTapItem: widget.items![index].onTapItem, + onDragItem: widget.items![index].onDragItem, + onStartDragItem: widget.items![index].onStartDragItem, + ); + } + if (widget.boardView!.draggedItemIndex == index && + widget.boardView!.draggedListIndex == widget.index) { + return Opacity( + opacity: 0.0, + child: widget.items![index], + ); + } else { + return widget.items![index]; + } + }, + ))); + } + + if (widget.footer != null) { + listWidgets.add(widget.footer!); + } + + Color? backgroundColor = const Color.fromARGB(255, 255, 255, 255); + + if (widget.backgroundColor != null) { + backgroundColor = widget.backgroundColor; + } + if (widget.boardView!.listStates.length > widget.index!) { + widget.boardView!.listStates.removeAt(widget.index!); + } + widget.boardView!.listStates.insert(widget.index!, this); + + return Container( + margin: const EdgeInsets.all(8), + decoration: BoxDecoration(color: backgroundColor), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: listWidgets, + )); + } +} diff --git a/demos/supabase-trello/lib/widgets/thirdparty/boardview.dart b/demos/supabase-trello/lib/widgets/thirdparty/boardview.dart new file mode 100644 index 00000000..7e6f9478 --- /dev/null +++ b/demos/supabase-trello/lib/widgets/thirdparty/boardview.dart @@ -0,0 +1,794 @@ +library boardview; + +import 'boardview_controller.dart'; +import 'vs_scrollbar.dart'; +import 'package:flutter/material.dart'; +import 'dart:core'; +import 'board_list.dart'; + +// ignore: must_be_immutable +class BoardView extends StatefulWidget { + final List? lists; + final double width; + Widget? middleWidget; + double? bottomPadding; + bool isSelecting; + bool? scrollbar; + ScrollbarStyle? scrollbarStyle; + BoardViewController? boardViewController; + int dragDelay; + + Function(bool)? itemInMiddleWidget; + OnDropBottomWidget? onDropItemInMiddleWidget; + BoardView( + {super.key, + this.itemInMiddleWidget, + this.scrollbar, + this.scrollbarStyle, + this.boardViewController, + this.dragDelay = 300, + this.onDropItemInMiddleWidget, + this.isSelecting = false, + this.lists, + this.width = 280, + this.middleWidget, + this.bottomPadding}); + + @override + State createState() { + return BoardViewState(); + } +} + +typedef OnDropBottomWidget = void Function( + int? listIndex, int? itemIndex, double percentX); +typedef OnDropItem = void Function(int? listIndex, int? itemIndex); +typedef OnDropList = void Function(int? listIndex); + +class BoardViewState extends State + with AutomaticKeepAliveClientMixin { + Widget? draggedItem; + int? draggedItemIndex; + int? draggedListIndex; + double? dx; + double? dxInit; + double? dyInit; + double? dy; + double? offsetX; + double? offsetY; + double? initialX = 0; + double? initialY = 0; + double? rightListX; + double? leftListX; + double? topListY; + double? bottomListY; + double? topItemY; + double? bottomItemY; + double? height; + int? startListIndex; + int? startItemIndex; + + bool canDrag = true; + + ScrollController boardViewController = ScrollController(); + + List listStates = []; + + OnDropItem? onDropItem; + OnDropList? onDropList; + + bool isScrolling = false; + + bool _isInWidget = false; + + final GlobalKey _middleWidgetKey = GlobalKey(); + + // ignore: prefer_typing_uninitialized_variables + var pointer; + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + if (widget.boardViewController != null) { + widget.boardViewController!.state = this; + } + } + + void moveDown() { + if (topItemY != null) { + topItemY = topItemY! + + listStates[draggedListIndex!] + .itemStates[draggedItemIndex! + 1] + .height; + } + if (bottomItemY != null) { + bottomItemY = bottomItemY! + + listStates[draggedListIndex!] + .itemStates[draggedItemIndex! + 1] + .height; + } + var item = widget.lists![draggedListIndex!].items![draggedItemIndex!]; + widget.lists![draggedListIndex!].items!.removeAt(draggedItemIndex!); + var itemState = listStates[draggedListIndex!].itemStates[draggedItemIndex!]; + listStates[draggedListIndex!].itemStates.removeAt(draggedItemIndex!); + if (draggedItemIndex != null) { + draggedItemIndex = draggedItemIndex! + 1; + } + widget.lists![draggedListIndex!].items!.insert(draggedItemIndex!, item); + listStates[draggedListIndex!] + .itemStates + .insert(draggedItemIndex!, itemState); + if (listStates[draggedListIndex!].mounted) { + listStates[draggedListIndex!].setState(() {}); + } + } + + void moveUp() { + if (topItemY != null) { + topItemY = topItemY! - + listStates[draggedListIndex!] + .itemStates[draggedItemIndex! - 1] + .height; + } + if (bottomItemY != null) { + bottomItemY = bottomItemY! - + listStates[draggedListIndex!] + .itemStates[draggedItemIndex! - 1] + .height; + } + var item = widget.lists![draggedListIndex!].items![draggedItemIndex!]; + widget.lists![draggedListIndex!].items!.removeAt(draggedItemIndex!); + var itemState = listStates[draggedListIndex!].itemStates[draggedItemIndex!]; + listStates[draggedListIndex!].itemStates.removeAt(draggedItemIndex!); + if (draggedItemIndex != null) { + draggedItemIndex = draggedItemIndex! - 1; + } + widget.lists![draggedListIndex!].items!.insert(draggedItemIndex!, item); + listStates[draggedListIndex!] + .itemStates + .insert(draggedItemIndex!, itemState); + if (listStates[draggedListIndex!].mounted) { + listStates[draggedListIndex!].setState(() {}); + } + } + + void moveListRight() { + var list = widget.lists![draggedListIndex!]; + var listState = listStates[draggedListIndex!]; + widget.lists!.removeAt(draggedListIndex!); + listStates.removeAt(draggedListIndex!); + if (draggedListIndex != null) { + draggedListIndex = draggedListIndex! + 1; + } + widget.lists!.insert(draggedListIndex!, list); + listStates.insert(draggedListIndex!, listState); + canDrag = false; + if (boardViewController.hasClients) { + int? tempListIndex = draggedListIndex; + boardViewController + .animateTo(draggedListIndex! * widget.width, + duration: const Duration(milliseconds: 400), curve: Curves.ease) + .whenComplete(() { + RenderBox object = + // ignore: use_build_context_synchronously + listStates[tempListIndex!].context.findRenderObject() as RenderBox; + Offset pos = object.localToGlobal(Offset.zero); + leftListX = pos.dx; + rightListX = pos.dx + object.size.width; + Future.delayed(Duration(milliseconds: widget.dragDelay), () { + canDrag = true; + }); + }); + } + if (mounted) { + setState(() {}); + } + } + + void moveRight() { + var item = widget.lists![draggedListIndex!].items![draggedItemIndex!]; + var itemState = listStates[draggedListIndex!].itemStates[draggedItemIndex!]; + widget.lists![draggedListIndex!].items!.removeAt(draggedItemIndex!); + listStates[draggedListIndex!].itemStates.removeAt(draggedItemIndex!); + if (listStates[draggedListIndex!].mounted) { + listStates[draggedListIndex!].setState(() {}); + } + if (draggedListIndex != null) { + draggedListIndex = draggedListIndex! + 1; + } + double closestValue = 10000; + draggedItemIndex = 0; + for (int i = 0; i < listStates[draggedListIndex!].itemStates.length; i++) { + if (listStates[draggedListIndex!].itemStates[i].mounted) { + RenderBox box = listStates[draggedListIndex!] + .itemStates[i] + .context + .findRenderObject() as RenderBox; + Offset pos = box.localToGlobal(Offset.zero); + var temp = (pos.dy - dy! + (box.size.height / 2)).abs(); + if (temp < closestValue) { + closestValue = temp; + draggedItemIndex = i; + dyInit = dy; + } + } + } + widget.lists![draggedListIndex!].items!.insert(draggedItemIndex!, item); + listStates[draggedListIndex!] + .itemStates + .insert(draggedItemIndex!, itemState); + canDrag = false; + if (listStates[draggedListIndex!].mounted) { + listStates[draggedListIndex!].setState(() {}); + } + if (boardViewController.hasClients) { + int? tempListIndex = draggedListIndex; + int? tempItemIndex = draggedItemIndex; + boardViewController + .animateTo(draggedListIndex! * widget.width, + duration: const Duration(milliseconds: 400), curve: Curves.ease) + .whenComplete(() { + RenderBox object = + // ignore: use_build_context_synchronously + listStates[tempListIndex!].context.findRenderObject() as RenderBox; + Offset pos = object.localToGlobal(Offset.zero); + leftListX = pos.dx; + rightListX = pos.dx + object.size.width; + // ignore: use_build_context_synchronously + RenderBox box = listStates[tempListIndex] + .itemStates[tempItemIndex!] + .context + .findRenderObject() as RenderBox; + Offset itemPos = box.localToGlobal(Offset.zero); + topItemY = itemPos.dy; + bottomItemY = itemPos.dy + box.size.height; + Future.delayed(Duration(milliseconds: widget.dragDelay), () { + canDrag = true; + }); + }); + } + if (mounted) { + setState(() {}); + } + } + + void moveListLeft() { + var list = widget.lists![draggedListIndex!]; + var listState = listStates[draggedListIndex!]; + widget.lists!.removeAt(draggedListIndex!); + listStates.removeAt(draggedListIndex!); + if (draggedListIndex != null) { + draggedListIndex = draggedListIndex! - 1; + } + widget.lists!.insert(draggedListIndex!, list); + listStates.insert(draggedListIndex!, listState); + canDrag = false; + if (boardViewController.hasClients) { + int? tempListIndex = draggedListIndex; + boardViewController + .animateTo(draggedListIndex! * widget.width, + duration: Duration(milliseconds: widget.dragDelay), + curve: Curves.ease) + .whenComplete(() { + RenderBox object = + // ignore: use_build_context_synchronously + listStates[tempListIndex!].context.findRenderObject() as RenderBox; + Offset pos = object.localToGlobal(Offset.zero); + leftListX = pos.dx; + rightListX = pos.dx + object.size.width; + Future.delayed(Duration(milliseconds: widget.dragDelay), () { + canDrag = true; + }); + }); + } + if (mounted) { + setState(() {}); + } + } + + void moveLeft() { + var item = widget.lists![draggedListIndex!].items![draggedItemIndex!]; + var itemState = listStates[draggedListIndex!].itemStates[draggedItemIndex!]; + widget.lists![draggedListIndex!].items!.removeAt(draggedItemIndex!); + listStates[draggedListIndex!].itemStates.removeAt(draggedItemIndex!); + if (listStates[draggedListIndex!].mounted) { + listStates[draggedListIndex!].setState(() {}); + } + if (draggedListIndex != null) { + draggedListIndex = draggedListIndex! - 1; + } + double closestValue = 10000; + draggedItemIndex = 0; + for (int i = 0; i < listStates[draggedListIndex!].itemStates.length; i++) { + if (listStates[draggedListIndex!].itemStates[i].mounted) { + RenderBox box = listStates[draggedListIndex!] + .itemStates[i] + .context + .findRenderObject() as RenderBox; + Offset pos = box.localToGlobal(Offset.zero); + var temp = (pos.dy - dy! + (box.size.height / 2)).abs(); + if (temp < closestValue) { + closestValue = temp; + draggedItemIndex = i; + dyInit = dy; + } + } + } + widget.lists![draggedListIndex!].items!.insert(draggedItemIndex!, item); + listStates[draggedListIndex!] + .itemStates + .insert(draggedItemIndex!, itemState); + canDrag = false; + if (listStates[draggedListIndex!].mounted) { + listStates[draggedListIndex!].setState(() {}); + } + if (boardViewController.hasClients) { + int? tempListIndex = draggedListIndex; + int? tempItemIndex = draggedItemIndex; + boardViewController + .animateTo(draggedListIndex! * widget.width, + duration: const Duration(milliseconds: 400), curve: Curves.ease) + .whenComplete(() { + RenderBox object = + // ignore: use_build_context_synchronously + listStates[tempListIndex!].context.findRenderObject() as RenderBox; + Offset pos = object.localToGlobal(Offset.zero); + leftListX = pos.dx; + rightListX = pos.dx + object.size.width; + // ignore: use_build_context_synchronously + RenderBox box = listStates[tempListIndex] + .itemStates[tempItemIndex!] + .context + .findRenderObject() as RenderBox; + Offset itemPos = box.localToGlobal(Offset.zero); + topItemY = itemPos.dy; + bottomItemY = itemPos.dy + box.size.height; + Future.delayed(Duration(milliseconds: widget.dragDelay), () { + canDrag = true; + }); + }); + } + if (mounted) { + setState(() {}); + } + } + + bool shown = true; + + @override + Widget build(BuildContext context) { + super.build(context); + if (boardViewController.hasClients) { + WidgetsBinding.instance.addPostFrameCallback((Duration duration) { + boardViewController.position.didUpdateScrollPositionBy(0); + // ignore: no_leading_underscores_for_local_identifiers + bool _shown = boardViewController.position.maxScrollExtent != 0; + if (_shown != shown) { + setState(() { + shown = _shown; + }); + } + }); + } + Widget listWidget = ListView.builder( + physics: const ClampingScrollPhysics(), + itemCount: widget.lists!.length, + scrollDirection: Axis.horizontal, + controller: boardViewController, + itemBuilder: (BuildContext context, int index) { + // if index does not exist on lists + if (widget.lists!.length <= index) { + return Container(); + } + + if (widget.lists![index].boardView == null) { + widget.lists![index] = BoardList( + items: widget.lists![index].items, + headerBackgroundColor: widget.lists![index].headerBackgroundColor, + backgroundColor: widget.lists![index].backgroundColor, + footer: widget.lists![index].footer, + header: widget.lists![index].header, + boardView: this, + draggable: widget.lists![index].draggable, + onDropList: widget.lists![index].onDropList, + onTapList: widget.lists![index].onTapList, + onStartDragList: widget.lists![index].onStartDragList, + ); + } + if (widget.lists![index].index != index) { + widget.lists![index] = BoardList( + items: widget.lists![index].items, + headerBackgroundColor: widget.lists![index].headerBackgroundColor, + backgroundColor: widget.lists![index].backgroundColor, + footer: widget.lists![index].footer, + header: widget.lists![index].header, + boardView: this, + draggable: widget.lists![index].draggable, + index: index, + onDropList: widget.lists![index].onDropList, + onTapList: widget.lists![index].onTapList, + onStartDragList: widget.lists![index].onStartDragList, + ); + } + + var temp = Container( + width: widget.width, + padding: EdgeInsets.fromLTRB(0, 0, 0, widget.bottomPadding ?? 0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [Expanded(child: widget.lists![index])], + )); + if (draggedListIndex == index && draggedItemIndex == null) { + return Opacity( + opacity: 0.0, + child: temp, + ); + } else { + return temp; + } + }, + ); + if (widget.scrollbar == true) { + listWidget = VsScrollbar( + controller: boardViewController, + showTrackOnHover: true, // default false + isAlwaysShown: shown && widget.lists!.length > 1, // default false + scrollbarFadeDuration: const Duration( + milliseconds: 500), // default : Duration(milliseconds: 300) + scrollbarTimeToFade: const Duration( + milliseconds: 800), // default : Duration(milliseconds: 600) + style: widget.scrollbarStyle != null + ? VsScrollbarStyle( + hoverThickness: widget.scrollbarStyle!.hoverThickness, + radius: widget.scrollbarStyle!.radius, + thickness: widget.scrollbarStyle!.thickness, + color: widget.scrollbarStyle!.color) + : const VsScrollbarStyle(), + child: listWidget); + } + List stackWidgets = [listWidget]; + bool isInBottomWidget = false; + if (dy != null) { + if (MediaQuery.of(context).size.height - dy! < 80) { + isInBottomWidget = true; + } + } + if (widget.itemInMiddleWidget != null && _isInWidget != isInBottomWidget) { + widget.itemInMiddleWidget!(isInBottomWidget); + _isInWidget = isInBottomWidget; + } + if (initialX != null && + initialY != null && + offsetX != null && + offsetY != null && + dx != null && + dy != null && + height != null) { + if (canDrag && dxInit != null && dyInit != null && !isInBottomWidget) { + if (draggedItemIndex != null && + draggedItem != null && + topItemY != null && + bottomItemY != null) { + //dragging item + if (0 <= draggedListIndex! - 1 && dx! < leftListX! + 45) { + //scroll left + if (boardViewController.hasClients) { + boardViewController.animateTo( + boardViewController.position.pixels - 5, + duration: const Duration(milliseconds: 10), + curve: Curves.ease); + if (listStates[draggedListIndex!].mounted) { + RenderBox object = listStates[draggedListIndex!] + .context + .findRenderObject() as RenderBox; + Offset pos = object.localToGlobal(Offset.zero); + leftListX = pos.dx; + rightListX = pos.dx + object.size.width; + } + } + } + if (widget.lists!.length > draggedListIndex! + 1 && + dx! > rightListX! - 45) { + //scroll right + if (boardViewController.hasClients) { + boardViewController.animateTo( + boardViewController.position.pixels + 5, + duration: const Duration(milliseconds: 10), + curve: Curves.ease); + if (listStates[draggedListIndex!].mounted) { + RenderBox object = listStates[draggedListIndex!] + .context + .findRenderObject() as RenderBox; + Offset pos = object.localToGlobal(Offset.zero); + leftListX = pos.dx; + rightListX = pos.dx + object.size.width; + } + } + } + if (0 <= draggedListIndex! - 1 && dx! < leftListX!) { + //move left + moveLeft(); + } + if (widget.lists!.length > draggedListIndex! + 1 && + dx! > rightListX!) { + //move right + moveRight(); + } + if (dy! < topListY! + 70) { + //scroll up + if (listStates[draggedListIndex!].boardListController.hasClients && + !isScrolling) { + isScrolling = true; + double pos = listStates[draggedListIndex!] + .boardListController + .position + .pixels; + listStates[draggedListIndex!] + .boardListController + .animateTo( + listStates[draggedListIndex!] + .boardListController + .position + .pixels - + 5, + duration: const Duration(milliseconds: 10), + curve: Curves.ease) + .whenComplete(() { + pos -= listStates[draggedListIndex!] + .boardListController + .position + .pixels; + initialY ??= 0; +// if(widget.boardViewController != null) { +// initialY -= pos; +// } + isScrolling = false; + if (topItemY != null) { + topItemY = topItemY! + pos; + } + if (bottomItemY != null) { + bottomItemY = bottomItemY! + pos; + } + if (mounted) { + setState(() {}); + } + }); + } + } + if (0 <= draggedItemIndex! - 1 && + dy! < + topItemY! - + listStates[draggedListIndex!] + .itemStates[draggedItemIndex! - 1] + .height / + 2) { + //move up + moveUp(); + } + double? tempBottom = bottomListY; + if (widget.middleWidget != null) { + if (_middleWidgetKey.currentContext != null) { + // ignore: no_leading_underscores_for_local_identifiers + RenderBox _box = _middleWidgetKey.currentContext! + .findRenderObject() as RenderBox; + tempBottom = _box.size.height; + } + } + if (dy! > tempBottom! - 70) { + //scroll down + + if (listStates.length < draggedListIndex! && + listStates[draggedListIndex!].boardListController.hasClients) { + isScrolling = true; + double pos = listStates[draggedListIndex!] + .boardListController + .position + .pixels; + listStates[draggedListIndex!] + .boardListController + .animateTo( + listStates[draggedListIndex!] + .boardListController + .position + .pixels + + 5, + duration: const Duration(milliseconds: 10), + curve: Curves.ease) + .whenComplete(() { + pos -= listStates[draggedListIndex!] + .boardListController + .position + .pixels; + initialY ??= 0; + isScrolling = false; + if (topItemY != null) { + topItemY = topItemY! + pos; + } + if (bottomItemY != null) { + bottomItemY = bottomItemY! + pos; + } + if (mounted) { + setState(() {}); + } + }); + } + } + if (widget.lists![draggedListIndex!].items!.length > + draggedItemIndex! + 1 && + dy! > + bottomItemY! + + listStates[draggedListIndex!] + .itemStates[draggedItemIndex! + 1] + .height / + 2) { + //move down + moveDown(); + } + } else { + //dragging list + if (0 <= draggedListIndex! - 1 && dx! < leftListX! + 45) { + //scroll left + if (boardViewController.hasClients) { + boardViewController.animateTo( + boardViewController.position.pixels - 5, + duration: const Duration(milliseconds: 10), + curve: Curves.ease); + if (leftListX != null) { + leftListX = leftListX! + 5; + } + if (rightListX != null) { + rightListX = rightListX! + 5; + } + } + } + + if (widget.lists!.length > draggedListIndex! + 1 && + dx! > rightListX! - 45) { + //scroll right + if (boardViewController.hasClients) { + boardViewController.animateTo( + boardViewController.position.pixels + 5, + duration: const Duration(milliseconds: 10), + curve: Curves.ease); + if (leftListX != null) { + leftListX = leftListX! - 5; + } + if (rightListX != null) { + rightListX = rightListX! - 5; + } + } + } + if (widget.lists!.length > draggedListIndex! + 1 && + dx! > rightListX!) { + //move right + moveListRight(); + } + if (0 <= draggedListIndex! - 1 && dx! < leftListX!) { + //move left + moveListLeft(); + } + } + } + if (widget.middleWidget != null) { + stackWidgets + .add(Container(key: _middleWidgetKey, child: widget.middleWidget)); + } + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + if (mounted) { + setState(() {}); + } + }); + stackWidgets.add(Positioned( + width: widget.width, + height: height, + left: (dx! - offsetX!) + initialX!, + top: (dy! - offsetY!) + initialY!, + child: Opacity(opacity: .7, child: draggedItem), + )); + } + + return Listener( + onPointerMove: (opm) { + if (draggedItem != null) { + dxInit ??= opm.position.dx; + dyInit ??= opm.position.dy; + dx = opm.position.dx; + dy = opm.position.dy; + if (mounted) { + setState(() {}); + } + } + }, + onPointerDown: (opd) { + RenderBox box = context.findRenderObject() as RenderBox; + Offset pos = box.localToGlobal(opd.position); + offsetX = pos.dx; + offsetY = pos.dy; + pointer = opd; + if (mounted) { + setState(() {}); + } + }, + onPointerUp: (opu) { + if (onDropItem != null) { + int? tempDraggedItemIndex = draggedItemIndex; + int? tempDraggedListIndex = draggedListIndex; + int? startDraggedItemIndex = startItemIndex; + int? startDraggedListIndex = startListIndex; + + if (_isInWidget && widget.onDropItemInMiddleWidget != null) { + onDropItem!(startDraggedListIndex, startDraggedItemIndex); + widget.onDropItemInMiddleWidget!( + startDraggedListIndex, + startDraggedItemIndex, + opu.position.dx / MediaQuery.of(context).size.width); + } else { + onDropItem!(tempDraggedListIndex, tempDraggedItemIndex); + } + } + if (onDropList != null) { + int? tempDraggedListIndex = draggedListIndex; + if (_isInWidget && widget.onDropItemInMiddleWidget != null) { + onDropList!(tempDraggedListIndex); + widget.onDropItemInMiddleWidget!(tempDraggedListIndex, null, + opu.position.dx / MediaQuery.of(context).size.width); + } else { + onDropList!(tempDraggedListIndex); + } + } + draggedItem = null; + offsetX = null; + offsetY = null; + initialX = null; + initialY = null; + dx = null; + dy = null; + draggedItemIndex = null; + draggedListIndex = null; + onDropItem = null; + onDropList = null; + dxInit = null; + dyInit = null; + leftListX = null; + rightListX = null; + topListY = null; + bottomListY = null; + topItemY = null; + bottomItemY = null; + startListIndex = null; + startItemIndex = null; + if (mounted) { + setState(() {}); + } + }, + child: Stack( + children: stackWidgets, + )); + } + + void run() { + if (pointer != null) { + dx = pointer.position.dx; + dy = pointer.position.dy; + if (mounted) { + setState(() {}); + } + } + } +} + +class ScrollbarStyle { + double hoverThickness; + double thickness; + Radius radius; + Color color; + ScrollbarStyle( + {this.radius = const Radius.circular(10), + this.hoverThickness = 10, + this.thickness = 10, + this.color = Colors.black}); +} diff --git a/demos/supabase-trello/lib/widgets/thirdparty/boardview_controller.dart b/demos/supabase-trello/lib/widgets/thirdparty/boardview_controller.dart new file mode 100644 index 00000000..8c93bd7e --- /dev/null +++ b/demos/supabase-trello/lib/widgets/thirdparty/boardview_controller.dart @@ -0,0 +1,17 @@ +import 'package:flutter/animation.dart'; + +import 'boardview.dart'; + +class BoardViewController { + BoardViewController(); + + late BoardViewState state; + + Future animateTo(int index, {Duration? duration, Curve? curve}) async { + double offset = index * state.widget.width; + if (state.boardViewController.hasClients) { + await state.boardViewController + .animateTo(offset, duration: duration!, curve: curve!); + } + } +} diff --git a/demos/supabase-trello/lib/widgets/thirdparty/vs_scrollbar.dart b/demos/supabase-trello/lib/widgets/thirdparty/vs_scrollbar.dart new file mode 100644 index 00000000..18cd4fc8 --- /dev/null +++ b/demos/supabase-trello/lib/widgets/thirdparty/vs_scrollbar.dart @@ -0,0 +1,345 @@ +// ignore_for_file: deprecated_member_use + +library vs_scrollbar; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +class VsScrollbarStyle { + /// The hoverThickness of the VsScrollbar thumb. + /// default value is 12.0 pixels. + final double hoverThickness; + + /// The thickness of the VsScrollbar thumb. + /// default thickness of 8.0 pixels. + final double thickness; + + /// The radius of the VsScrollbar thumb. + /// default [Radius.circular] of 8.0 pixels. + final Radius radius; + + /// The color of the VsScrollbar thumb. + final Color? color; + + const VsScrollbarStyle( + {this.radius = _kScrollbarRadius, + this.thickness = _kScrollbarThickness, + this.hoverThickness = _kScrollbarThicknessWithTrack, + this.color}); +} + +const VsScrollbarStyle _kScrollbarStyle = VsScrollbarStyle(); +const double _kScrollbarThickness = 8.0; +const double _kScrollbarThicknessWithTrack = 12.0; +const double _kScrollbarMargin = 2.0; +const double _kScrollbarMinLength = 48.0; +const Radius _kScrollbarRadius = Radius.circular(8.0); +const Duration _kScrollbarFadeDuration = Duration(milliseconds: 300); +const Duration _kScrollbarTimeToFade = Duration(milliseconds: 600); + +/// To add a VsScrollbar to a [ScrollView], wrap the scroll view +/// widget in a [VsScrollbar] widget. +/// +/// The color of the VsScrollbar will change when dragged. A hover animation is +/// also triggered when used on web and desktop platforms. A VsScrollbar track +/// can also been drawn when triggered by a hover event, which is controlled by +/// [showTrackOnHover]. The thickness of the track and VsScrollbar thumb will +/// become larger when hovering, unless overridden by [hoverThickness]. +/// +/// See also: +/// +/// * [RawScrollbar], a basic VsScrollbar that fades in and out, extended +/// by this class to add more animations and behaviors. +/// * [ScrollbarTheme], which configures the VsScrollbar's appearance. +/// * [ListView], which displays a linear, scrollable list of children. +/// * [GridView], which displays a 2 dimensional, scrollable array of children. +class VsScrollbar extends StatefulWidget { + /// Creates a material design VsScrollbar that by default will connect to the + /// closest Scrollable descendant of [child]. + /// + /// The [child] should be a source of [ScrollNotification] notifications, + /// typically a [Scrollable] widget. + /// + /// If the [controller] is null, the default behavior is to + /// enable VsScrollbar dragging using the [PrimaryScrollController]. + /// + const VsScrollbar({ + super.key, + required this.child, + this.controller, + this.style = _kScrollbarStyle, + this.scrollbarFadeDuration, + this.scrollbarTimeToFade, + this.isAlwaysShown, + this.showTrackOnHover, + this.notificationPredicate, + }); + + /// {@macro flutter.widgets.VsScrollbar.child} + final Widget child; + + /// {@macro flutter.widgets.VsScrollbar.controller} + final ScrollController? controller; + + /// {@macro flutter.widgets.VsScrollbar.isAlwaysShown} + final bool? isAlwaysShown; + + /// If this property is null, then [ScrollbarThemeData.showTrackOnHover] of + /// [ThemeData.scrollbarTheme] is used. If that is also null, the default value + /// is false. + final bool? showTrackOnHover; + + ///Style Property for VsScrollbar + final VsScrollbarStyle style; + + /// {@macro flutter.widgets.VsScrollbar.notificationPredicate} + final ScrollNotificationPredicate? notificationPredicate; + + /// default 600ms + final Duration? scrollbarTimeToFade; + + /// default 300ms + final Duration? scrollbarFadeDuration; + + @override + // ignore: library_private_types_in_public_api + _ScrollbarState createState() => _ScrollbarState(); +} + +class _ScrollbarState extends State { + @override + Widget build(BuildContext context) { + return _MaterialScrollbar( + controller: widget.controller, + isAlwaysShown: widget.isAlwaysShown, + showTrackOnHover: widget.showTrackOnHover, + hoverThickness: widget.style.hoverThickness, + thickness: widget.style.thickness, + radius: widget.style.radius, + color: widget.style.color, + notificationPredicate: widget.notificationPredicate, + child: widget.child, + ); + } +} + +class _MaterialScrollbar extends RawScrollbar { + const _MaterialScrollbar({ + required super.child, + super.controller, + bool? isAlwaysShown, + this.showTrackOnHover, + this.hoverThickness, + this.color, + this.scrollbarFadeDuration, + this.scrollbarTimeToFade, + super.thickness, + super.radius, + ScrollNotificationPredicate? notificationPredicate, + }) : super( + thumbVisibility: isAlwaysShown, + fadeDuration: scrollbarFadeDuration ?? _kScrollbarFadeDuration, + timeToFade: scrollbarTimeToFade ?? _kScrollbarTimeToFade, + pressDuration: Duration.zero, + notificationPredicate: + notificationPredicate ?? defaultScrollNotificationPredicate, + ); + final Duration? scrollbarTimeToFade; + final Duration? scrollbarFadeDuration; + + final Color? color; + final bool? showTrackOnHover; + final double? hoverThickness; + + @override + _MaterialScrollbarState createState() => _MaterialScrollbarState(); +} + +class _MaterialScrollbarState extends RawScrollbarState<_MaterialScrollbar> { + late AnimationController _hoverAnimationController; + bool _dragIsActive = true; + bool _hoverIsActive = false; + late ColorScheme _colorScheme; + late ScrollbarThemeData _scrollbarTheme; + // On Android, scrollbars should match native appearance. + late bool _useAndroidScrollbar; + + @override + bool get showScrollbar => + widget.thumbVisibility ?? + _scrollbarTheme.thumbVisibility?.resolve({WidgetState.disabled}) ?? + false; + + bool get _showTrackOnHover => widget.showTrackOnHover ?? false; + + Set get _states => { + if (_dragIsActive) WidgetState.dragged, + if (_hoverIsActive) WidgetState.hovered, + }; + + WidgetStateProperty get _thumbColor { + final Color onSurface = widget.color ?? _colorScheme.onSurface; + late Color dragColor; + late Color hoverColor; + late Color idleColor; + dragColor = onSurface.withOpacity(0.9); + hoverColor = onSurface.withOpacity(0.75); + idleColor = onSurface.withOpacity(0.5); + + return WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.dragged)) { + return _scrollbarTheme.thumbColor?.resolve(states) ?? dragColor; + } + + // If the track is visible, the thumb color hover animation is ignored and + // changes immediately. + if (states.contains(WidgetState.hovered) && _showTrackOnHover) { + return _scrollbarTheme.thumbColor?.resolve(states) ?? hoverColor; + } + + return Color.lerp( + _scrollbarTheme.thumbColor?.resolve(states) ?? idleColor, + _scrollbarTheme.thumbColor?.resolve(states) ?? hoverColor, + _hoverAnimationController.value, + )!; + }); + } + + WidgetStateProperty get _trackColor { + final Color onSurface = widget.color ?? _colorScheme.onSurface; + + return WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.hovered) && _showTrackOnHover) { + return _scrollbarTheme.trackColor?.resolve(states) ?? + onSurface.withOpacity(0.05); + } + return const Color(0x00000000); + }); + } + + WidgetStateProperty get _trackBorderColor { + final Color onSurface = widget.color ?? _colorScheme.onSurface; + + return WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.hovered) && _showTrackOnHover) { + return _scrollbarTheme.trackBorderColor?.resolve(states) ?? + onSurface.withOpacity(0.1); + } + return const Color(0x00000000); + }); + } + + WidgetStateProperty get _thickness { + return WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.hovered) && _showTrackOnHover) { + return widget.hoverThickness ?? + _scrollbarTheme.thickness?.resolve(states) ?? + _kScrollbarThicknessWithTrack; + } + // The default VsScrollbar thickness is smaller on mobile. + return widget.thickness ?? + _scrollbarTheme.thickness?.resolve(states) ?? + (_kScrollbarThickness / (_useAndroidScrollbar ? 2 : 1)); + }); + } + + @override + void initState() { + super.initState(); + _hoverAnimationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 200), + ); + _hoverAnimationController.addListener(() { + updateScrollbarPainter(); + }); + } + + @override + void didChangeDependencies() { + final ThemeData theme = Theme.of(context); + _colorScheme = theme.colorScheme; + _scrollbarTheme = theme.scrollbarTheme; + switch (theme.platform) { + case TargetPlatform.android: + _useAndroidScrollbar = true; + break; + case TargetPlatform.iOS: + case TargetPlatform.linux: + case TargetPlatform.fuchsia: + case TargetPlatform.macOS: + case TargetPlatform.windows: + _useAndroidScrollbar = false; + break; + } + super.didChangeDependencies(); + } + + @override + void updateScrollbarPainter() { + scrollbarPainter + ..color = _thumbColor.resolve(_states) + ..trackColor = _trackColor.resolve(_states) + ..trackBorderColor = _trackBorderColor.resolve(_states) + ..textDirection = Directionality.of(context) + ..thickness = _thickness.resolve(_states) + ..radius = widget.radius ?? + _scrollbarTheme.radius ?? + (_useAndroidScrollbar ? null : _kScrollbarRadius) + ..crossAxisMargin = _scrollbarTheme.crossAxisMargin ?? + (_useAndroidScrollbar ? 0.0 : _kScrollbarMargin) + ..mainAxisMargin = _scrollbarTheme.mainAxisMargin ?? 0.0 + ..minLength = _scrollbarTheme.minThumbLength ?? _kScrollbarMinLength + ..padding = const EdgeInsets.all(0); + } + + @override + void handleThumbPressStart(Offset localPosition) { + super.handleThumbPressStart(localPosition); + setState(() { + _dragIsActive = true; + }); + } + + @override + void handleThumbPressEnd(Offset localPosition, Velocity velocity) { + super.handleThumbPressEnd(localPosition, velocity); + setState(() { + _dragIsActive = false; + }); + } + + @override + void handleHover(PointerHoverEvent event) { + super.handleHover(event); + // Check if the position of the pointer falls over the painted VsScrollbar + if (isPointerOverScrollbar(event.position, PointerDeviceKind.mouse)) { + // Pointer is hovering over the VsScrollbar + setState(() { + _hoverIsActive = true; + }); + _hoverAnimationController.forward(); + } else if (_hoverIsActive) { + // Pointer was, but is no longer over painted VsScrollbar. + setState(() { + _hoverIsActive = false; + }); + _hoverAnimationController.reverse(); + } + } + + @override + void handleHoverExit(PointerExitEvent event) { + super.handleHoverExit(event); + setState(() { + _hoverIsActive = false; + }); + _hoverAnimationController.reverse(); + } + + @override + void dispose() { + _hoverAnimationController.dispose(); + super.dispose(); + } +} diff --git a/demos/supabase-trello/linux/.gitignore b/demos/supabase-trello/linux/.gitignore new file mode 100644 index 00000000..d3896c98 --- /dev/null +++ b/demos/supabase-trello/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/demos/supabase-trello/linux/CMakeLists.txt b/demos/supabase-trello/linux/CMakeLists.txt new file mode 100644 index 00000000..edd5d8f3 --- /dev/null +++ b/demos/supabase-trello/linux/CMakeLists.txt @@ -0,0 +1,139 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "trelloappclone_flutter") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.trelloappclone_flutter") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/demos/supabase-trello/linux/flutter/CMakeLists.txt b/demos/supabase-trello/linux/flutter/CMakeLists.txt new file mode 100644 index 00000000..d5bd0164 --- /dev/null +++ b/demos/supabase-trello/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/demos/supabase-trello/linux/flutter/generated_plugin_registrant.cc b/demos/supabase-trello/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..129b6b2f --- /dev/null +++ b/demos/supabase-trello/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,31 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); + g_autoptr(FlPluginRegistrar) powersync_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "PowersyncFlutterLibsPlugin"); + powersync_flutter_libs_plugin_register_with_registrar(powersync_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); + sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/demos/supabase-trello/linux/flutter/generated_plugin_registrant.h b/demos/supabase-trello/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..e0f0a47b --- /dev/null +++ b/demos/supabase-trello/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/demos/supabase-trello/linux/flutter/generated_plugins.cmake b/demos/supabase-trello/linux/flutter/generated_plugins.cmake new file mode 100644 index 00000000..c9e9c841 --- /dev/null +++ b/demos/supabase-trello/linux/flutter/generated_plugins.cmake @@ -0,0 +1,28 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux + gtk + powersync_flutter_libs + sqlite3_flutter_libs + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/demos/supabase-trello/linux/main.cc b/demos/supabase-trello/linux/main.cc new file mode 100644 index 00000000..e7c5c543 --- /dev/null +++ b/demos/supabase-trello/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/demos/supabase-trello/linux/my_application.cc b/demos/supabase-trello/linux/my_application.cc new file mode 100644 index 00000000..e91e0138 --- /dev/null +++ b/demos/supabase-trello/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "trelloappclone_flutter"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "trelloappclone_flutter"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/demos/supabase-trello/linux/my_application.h b/demos/supabase-trello/linux/my_application.h new file mode 100644 index 00000000..72271d5e --- /dev/null +++ b/demos/supabase-trello/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/demos/supabase-trello/macos/.gitignore b/demos/supabase-trello/macos/.gitignore new file mode 100644 index 00000000..746adbb6 --- /dev/null +++ b/demos/supabase-trello/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/demos/supabase-trello/macos/Flutter/Flutter-Debug.xcconfig b/demos/supabase-trello/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 00000000..4b81f9b2 --- /dev/null +++ b/demos/supabase-trello/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/demos/supabase-trello/macos/Flutter/Flutter-Release.xcconfig b/demos/supabase-trello/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 00000000..5caa9d15 --- /dev/null +++ b/demos/supabase-trello/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/demos/supabase-trello/macos/Flutter/GeneratedPluginRegistrant.swift b/demos/supabase-trello/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 00000000..31370d7d --- /dev/null +++ b/demos/supabase-trello/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,26 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import app_links +import file_picker +import file_selector_macos +import path_provider_foundation +import powersync_flutter_libs +import shared_preferences_foundation +import sqlite3_flutter_libs +import url_launcher_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) + FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + PowersyncFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "PowersyncFlutterLibsPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) +} diff --git a/demos/supabase-trello/macos/Podfile b/demos/supabase-trello/macos/Podfile new file mode 100644 index 00000000..b52666a1 --- /dev/null +++ b/demos/supabase-trello/macos/Podfile @@ -0,0 +1,43 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/demos/supabase-trello/macos/Podfile.lock b/demos/supabase-trello/macos/Podfile.lock new file mode 100644 index 00000000..93449db4 --- /dev/null +++ b/demos/supabase-trello/macos/Podfile.lock @@ -0,0 +1,95 @@ +PODS: + - app_links (1.0.0): + - FlutterMacOS + - file_picker (0.0.1): + - FlutterMacOS + - file_selector_macos (0.0.1): + - FlutterMacOS + - FlutterMacOS (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - powersync-sqlite-core (0.4.5) + - powersync_flutter_libs (0.0.1): + - FlutterMacOS + - powersync-sqlite-core (~> 0.4.5) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (3.49.2): + - sqlite3/common (= 3.49.2) + - sqlite3/common (3.49.2) + - sqlite3/dbstatvtab (3.49.2): + - sqlite3/common + - sqlite3/fts5 (3.49.2): + - sqlite3/common + - sqlite3/math (3.49.2): + - sqlite3/common + - sqlite3/perf-threadsafe (3.49.2): + - sqlite3/common + - sqlite3/rtree (3.49.2): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (~> 3.49.1) + - sqlite3/dbstatvtab + - sqlite3/fts5 + - sqlite3/math + - sqlite3/perf-threadsafe + - sqlite3/rtree + - url_launcher_macos (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) + - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) + - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - powersync_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/powersync_flutter_libs/macos`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + +SPEC REPOS: + trunk: + - powersync-sqlite-core + - sqlite3 + +EXTERNAL SOURCES: + app_links: + :path: Flutter/ephemeral/.symlinks/plugins/app_links/macos + file_picker: + :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos + file_selector_macos: + :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos + FlutterMacOS: + :path: Flutter/ephemeral + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + powersync_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/powersync_flutter_libs/macos + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + sqlite3_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + +SPEC CHECKSUMS: + app_links: afe860c55c7ef176cea7fb630a2b7d7736de591d + file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a + file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + powersync-sqlite-core: 6f32860379009d2a37cadc9e9427a431bdbd83c8 + powersync_flutter_libs: 41d8a7b193abf15e46f95f0ec1229d86b6893171 + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 + sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 + +PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 + +COCOAPODS: 1.16.2 diff --git a/demos/supabase-trello/macos/Runner.xcodeproj/project.pbxproj b/demos/supabase-trello/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..410d47d9 --- /dev/null +++ b/demos/supabase-trello/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,791 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 4E4963C86BC785F23F648E0B /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A80D2BD607CA38846657D9B0 /* Pods_RunnerTests.framework */; }; + 9E95FABB7D4E5912BF4FE47B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D41298BB1DE04D5251142AE7 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 2D2712CAF1EB5DE8C09B8534 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* trelloappclone_flutter.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = trelloappclone_flutter.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 4D2CF99A3C595901E898A5A1 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 5E01E450DF23BB04BA05D6DF /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 7D86C2AB85DE03E2BC92BD82 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + 9F1765F95B32AB229EDC81A4 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + A80D2BD607CA38846657D9B0 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D1E469A34757CC55006C694C /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + D41298BB1DE04D5251142AE7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4E4963C86BC785F23F648E0B /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9E95FABB7D4E5912BF4FE47B /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 28F54DBEE7BF5990E862A72B /* Pods */ = { + isa = PBXGroup; + children = ( + 9F1765F95B32AB229EDC81A4 /* Pods-Runner.debug.xcconfig */, + 7D86C2AB85DE03E2BC92BD82 /* Pods-Runner.release.xcconfig */, + 5E01E450DF23BB04BA05D6DF /* Pods-Runner.profile.xcconfig */, + 2D2712CAF1EB5DE8C09B8534 /* Pods-RunnerTests.debug.xcconfig */, + 4D2CF99A3C595901E898A5A1 /* Pods-RunnerTests.release.xcconfig */, + D1E469A34757CC55006C694C /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 28F54DBEE7BF5990E862A72B /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* trelloappclone_flutter.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D41298BB1DE04D5251142AE7 /* Pods_Runner.framework */, + A80D2BD607CA38846657D9B0 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 125EE0D4B5332F47B3E8DC59 /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 95B0EDBAFB4503A28905DD2B /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + DFB0D194C13543172E694703 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* trelloappclone_flutter.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1430; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 125EE0D4B5332F47B3E8DC59 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 95B0EDBAFB4503A28905DD2B /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + DFB0D194C13543172E694703 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2D2712CAF1EB5DE8C09B8534 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.trelloappcloneFlutter.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/trelloappclone_flutter.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/trelloappclone_flutter"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4D2CF99A3C595901E898A5A1 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.trelloappcloneFlutter.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/trelloappclone_flutter.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/trelloappclone_flutter"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D1E469A34757CC55006C694C /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.trelloappcloneFlutter.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/trelloappclone_flutter.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/trelloappclone_flutter"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/demos/supabase-trello/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demos/supabase-trello/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/demos/supabase-trello/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/demos/supabase-trello/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demos/supabase-trello/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..9a756df2 --- /dev/null +++ b/demos/supabase-trello/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/supabase-trello/macos/Runner.xcworkspace/contents.xcworkspacedata b/demos/supabase-trello/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/demos/supabase-trello/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/demos/supabase-trello/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demos/supabase-trello/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/demos/supabase-trello/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/demos/supabase-trello/macos/Runner/AppDelegate.swift b/demos/supabase-trello/macos/Runner/AppDelegate.swift new file mode 100644 index 00000000..d53ef643 --- /dev/null +++ b/demos/supabase-trello/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..a2ec33f1 --- /dev/null +++ b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 00000000..82b6f9d9 Binary files /dev/null and b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 00000000..13b35eba Binary files /dev/null and b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 00000000..0a3f5fa4 Binary files /dev/null and b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 00000000..bdb57226 Binary files /dev/null and b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 00000000..f083318e Binary files /dev/null and b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 00000000..326c0e72 Binary files /dev/null and b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 00000000..2f1632cf Binary files /dev/null and b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/demos/supabase-trello/macos/Runner/Base.lproj/MainMenu.xib b/demos/supabase-trello/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 00000000..80e867a4 --- /dev/null +++ b/demos/supabase-trello/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/supabase-trello/macos/Runner/Configs/AppInfo.xcconfig b/demos/supabase-trello/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 00000000..47fffa43 --- /dev/null +++ b/demos/supabase-trello/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = trelloappclone_flutter + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.trelloappcloneFlutter + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2023 com.example. All rights reserved. diff --git a/demos/supabase-trello/macos/Runner/Configs/Debug.xcconfig b/demos/supabase-trello/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 00000000..36b0fd94 --- /dev/null +++ b/demos/supabase-trello/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/demos/supabase-trello/macos/Runner/Configs/Release.xcconfig b/demos/supabase-trello/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 00000000..dff4f495 --- /dev/null +++ b/demos/supabase-trello/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/demos/supabase-trello/macos/Runner/Configs/Warnings.xcconfig b/demos/supabase-trello/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 00000000..42bcbf47 --- /dev/null +++ b/demos/supabase-trello/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/demos/supabase-trello/macos/Runner/DebugProfile.entitlements b/demos/supabase-trello/macos/Runner/DebugProfile.entitlements new file mode 100644 index 00000000..08c3ab17 --- /dev/null +++ b/demos/supabase-trello/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + + diff --git a/demos/supabase-trello/macos/Runner/Info.plist b/demos/supabase-trello/macos/Runner/Info.plist new file mode 100644 index 00000000..4789daa6 --- /dev/null +++ b/demos/supabase-trello/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/demos/supabase-trello/macos/Runner/MainFlutterWindow.swift b/demos/supabase-trello/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 00000000..3cc05eb2 --- /dev/null +++ b/demos/supabase-trello/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/demos/supabase-trello/macos/Runner/Release.entitlements b/demos/supabase-trello/macos/Runner/Release.entitlements new file mode 100644 index 00000000..779a1789 --- /dev/null +++ b/demos/supabase-trello/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/demos/supabase-trello/macos/RunnerTests/RunnerTests.swift b/demos/supabase-trello/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..5418c9f5 --- /dev/null +++ b/demos/supabase-trello/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import FlutterMacOS +import Cocoa +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/demos/supabase-trello/pubspec.lock b/demos/supabase-trello/pubspec.lock new file mode 100644 index 00000000..191e94d1 --- /dev/null +++ b/demos/supabase-trello/pubspec.lock @@ -0,0 +1,951 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + app_links: + dependency: transitive + description: + name: app_links + sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba" + url: "https://pub.dev" + source: hosted + version: "6.4.0" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810 + url: "https://pub.dev" + source: hosted + version: "8.3.7" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" + url: "https://pub.dev" + source: hosted + version: "0.9.4+2" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" + url: "https://pub.dev" + source: hosted + version: "0.9.3+4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: b7c7be5cd9f6ef7a78429cabd2774d3c4af50e79cb2b7593e3d5d763ef95c61b + url: "https://pub.dev" + source: hosted + version: "5.2.1" + flutter_expandable_fab: + dependency: "direct main" + description: + name: flutter_expandable_fab + sha256: "9de10aad89ebff35956d8eb4ceb0d8749835dc1184d3ab17b721eb06c778c519" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e + url: "https://pub.dev" + source: hosted + version: "2.0.28" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + functions_client: + dependency: transitive + description: + name: functions_client + sha256: b410e4d609522357396cd84bb9a8f6e3a4561b5f7d3ce82267f6f1c2af42f16b + url: "https://pub.dev" + source: hosted + version: "2.4.2" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 + url: "https://pub.dev" + source: hosted + version: "6.2.1" + gotrue: + dependency: transitive + description: + name: gotrue + sha256: "04a6efacffd42773ed96dc752f19bb20a1fbc383e81ba82659072b775cf62912" + url: "https://pub.dev" + source: hosted + version: "2.12.0" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" + http: + dependency: transitive + description: + name: http + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "317a5d961cec5b34e777b9252393f2afbd23084aa6e60fcf601dcf6341b9ebeb" + url: "https://pub.dev" + source: hosted + version: "0.8.12+23" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" + url: "https://pub.dev" + source: hosted + version: "0.8.12+2" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9" + url: "https://pub.dev" + source: hosted + version: "0.2.1+2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" + url: "https://pub.dev" + source: hosted + version: "0.2.1+2" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" + url: "https://pub.dev" + source: hosted + version: "2.10.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + jwt_decode: + dependency: transitive + description: + name: jwt_decode + sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb + url: "https://pub.dev" + source: hosted + version: "0.3.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + url: "https://pub.dev" + source: hosted + version: "10.0.9" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.dev" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + logging: + dependency: "direct main" + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + material_design_icons_flutter: + dependency: "direct main" + description: + name: material_design_icons_flutter + sha256: "6f986b7a51f3ad4c00e33c5c84e8de1bdd140489bbcdc8b66fc1283dad4dea5a" + url: "https://pub.dev" + source: hosted + version: "7.0.7296" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mutex: + dependency: transitive + description: + name: mutex + sha256: "8827da25de792088eb33e572115a5eb0d61d61a3c01acbc8bcbe76ed78f1a1f2" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + path: + dependency: "direct main" + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + url: "https://pub.dev" + source: hosted + version: "2.2.17" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + postgrest: + dependency: transitive + description: + name: postgrest + sha256: "10b81a23b1c829ccadf68c626b4d66666453a1474d24c563f313f5ca7851d575" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + powersync: + dependency: "direct main" + description: + path: "../../packages/powersync" + relative: true + source: path + version: "1.15.0" + powersync_core: + dependency: "direct overridden" + description: + path: "../../packages/powersync_core" + relative: true + source: path + version: "1.5.0" + powersync_flutter_libs: + dependency: "direct overridden" + description: + path: "../../packages/powersync_flutter_libs" + relative: true + source: path + version: "0.4.10" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" + url: "https://pub.dev" + source: hosted + version: "6.1.5" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + random_name_generator: + dependency: "direct main" + description: + name: random_name_generator + sha256: "7c5b91d60f68b30e7b4c53006047cab8474f06563f7d0cab70fb409a0cb5ff61" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + realtime_client: + dependency: transitive + description: + name: realtime_client + sha256: "3a0a99b5bd0fc3b35e8ee846d9a22fa2c2117f7ef1cb73d1e5f08f6c3d09c4e9" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + retry: + dependency: transitive + description: + name: retry + sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" + url: "https://pub.dev" + source: hosted + version: "2.4.10" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e" + url: "https://pub.dev" + source: hosted + version: "2.7.5" + sqlite3_flutter_libs: + dependency: transitive + description: + name: sqlite3_flutter_libs + sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14" + url: "https://pub.dev" + source: hosted + version: "0.5.32" + sqlite3_web: + dependency: transitive + description: + name: sqlite3_web + sha256: "967e076442f7e1233bd7241ca61f3efe4c7fc168dac0f38411bdb3bdf471eb3c" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + sqlite_async: + dependency: "direct main" + description: + name: sqlite_async + sha256: a60e8d5c8df8e694933bd5a312c38393e79ad77d784bb91c6f38ba627bfb7aec + url: "https://pub.dev" + source: hosted + version: "0.11.4" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + storage_client: + dependency: transitive + description: + name: storage_client + sha256: "09bac4d75eea58e8113ca928e6655a09cc8059e6d1b472ee801f01fde815bcfc" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + supabase: + dependency: transitive + description: + name: supabase + sha256: f00172f5f0b2148ea1c573f52862d50cacb6f353f579f741fa35e51704845958 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + supabase_flutter: + dependency: "direct main" + description: + name: supabase_flutter + sha256: d88eccf9e46e57129725a08e72a3109b6f780921fdc27fe3d7669a11ae80906b + url: "https://pub.dev" + source: hosted + version: "2.9.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.dev" + source: hosted + version: "0.7.4" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" + url: "https://pub.dev" + source: hosted + version: "6.3.16" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" + url: "https://pub.dev" + source: hosted + version: "6.3.3" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + url: "https://pub.dev" + source: hosted + version: "15.0.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + win32: + dependency: transitive + description: + name: win32 + sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f + url: "https://pub.dev" + source: hosted + version: "5.12.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" + yet_another_json_isolate: + dependency: transitive + description: + name: yet_another_json_isolate + sha256: fe45897501fa156ccefbfb9359c9462ce5dec092f05e8a56109db30be864f01e + url: "https://pub.dev" + source: hosted + version: "2.1.0" +sdks: + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/demos/supabase-trello/pubspec.yaml b/demos/supabase-trello/pubspec.yaml new file mode 100644 index 00000000..a609124e --- /dev/null +++ b/demos/supabase-trello/pubspec.yaml @@ -0,0 +1,65 @@ +name: trelloappclone_flutter +description: A Flutter clone of Trello. + +# The following line prevents the package from being accidentally published to +# pub.dev using `pub publish`. This is preferred for private packages. +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 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +version: 1.0.3+1 + +environment: + sdk: ^3.4.0 + +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + google_fonts: ^6.2.1 + flutter_expandable_fab: ^2.3.0 + material_design_icons_flutter: ^7.0.7296 + provider: ^6.1.2 + image_picker: ^1.1.2 + file_picker: ^8.1.7 + random_name_generator: ^1.5.0 + flutter_dotenv: ^5.2.1 + logging: ^1.3.0 + powersync: ^1.16.1 + sqlite_async: ^0.12.0 + path_provider: ^2.1.5 + supabase_flutter: ^2.8.3 + path: ^1.9.0 + +dev_dependencies: + flutter_lints: ^3.0.1 + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - assets/landing.jpg + - assets/trello-logo.png + - .env diff --git a/demos/supabase-trello/sample_workspace.png b/demos/supabase-trello/sample_workspace.png new file mode 100644 index 00000000..7b92ef89 Binary files /dev/null and b/demos/supabase-trello/sample_workspace.png differ diff --git a/demos/supabase-trello/showcase.png b/demos/supabase-trello/showcase.png new file mode 100644 index 00000000..322044b8 Binary files /dev/null and b/demos/supabase-trello/showcase.png differ diff --git a/demos/supabase-trello/sync-rules-0.yaml b/demos/supabase-trello/sync-rules-0.yaml new file mode 100644 index 00000000..6eaeb007 --- /dev/null +++ b/demos/supabase-trello/sync-rules-0.yaml @@ -0,0 +1,19 @@ +#naive version with global access +bucket_definitions: + global: + parameters: | + SELECT FROM trellouser WHERE + trellouser.id = token_parameters.user_id + data: + - SELECT * FROM trellouser + - SELECT * FROM workspace + - SELECT * FROM board + - SELECT * FROM activity + - SELECT * FROM attachment + - SELECT * FROM card + - SELECT * FROM checklist + - SELECT * FROM comment + - SELECT * FROM listboard + - SELECT * FROM member + - SELECT * FROM board_label + - SELECT * FROM card_label diff --git a/demos/supabase-trello/sync-rules-1.yaml b/demos/supabase-trello/sync-rules-1.yaml new file mode 100644 index 00000000..1da8b4c2 --- /dev/null +++ b/demos/supabase-trello/sync-rules-1.yaml @@ -0,0 +1,27 @@ +bucket_definitions: + user_info: + # this allows syncing of all trellouser records so we can lookup users when adding members + data: + - SELECT * FROM trellouser + by_workspace: + # the entities are filtered by workspaceId, thus linked to the workspaces (a) owned by this user, (b) where this user is a member, or (c) which are public + # Note: the quotes for "workspaceId" and "userId" is important, since otherwise postgres does not deal well with non-lowercase identifiers + parameters: + - SELECT id as workspace_id FROM workspace WHERE + workspace."userId" = token_parameters.user_id # OR visibility = "Public" + - SELECT "workspaceId" as workspace_id FROM member WHERE + member."userId" = token_parameters.user_id + - SELECT id as workspace_id FROM workspace WHERE + visibility = "Public" + data: + - SELECT * FROM workspace WHERE workspace.id = bucket.workspace_id + - SELECT * FROM board WHERE board."workspaceId" = bucket.workspace_id + - SELECT * FROM member WHERE member."workspaceId" = bucket.workspace_id + - SELECT * FROM listboard WHERE listboard."workspaceId" = bucket.workspace_id + - SELECT * FROM card WHERE card."workspaceId" = bucket.workspace_id + - SELECT * FROM checklist WHERE checklist."workspaceId" = bucket.workspace_id + - SELECT * FROM activity WHERE activity."workspaceId" = bucket.workspace_id + - SELECT * FROM comment WHERE comment."workspaceId" = bucket.workspace_id + - SELECT * FROM attachment WHERE attachment."workspaceId" = bucket.workspace_id + - SELECT * FROM board_label WHERE board_label."workspaceId" = bucket.workspace_id + - SELECT * FROM card_label WHERE card_label."workspaceId" = bucket.workspace_id diff --git a/demos/supabase-trello/tables.sql b/demos/supabase-trello/tables.sql new file mode 100644 index 00000000..db0beca7 --- /dev/null +++ b/demos/supabase-trello/tables.sql @@ -0,0 +1,332 @@ +-- +-- Class User as table trellouser +-- + +CREATE TABLE "trellouser" ( + "id" uuid not null default gen_random_uuid (), + "name" text, + "email" text NOT NULL, + "password" text NOT NULL +); + +ALTER TABLE ONLY "trellouser" + ADD CONSTRAINT trellouser_pkey PRIMARY KEY (id); + + +-- +-- Class Workspace as table workspace +-- + +CREATE TABLE "workspace" ( + "id" uuid not null default gen_random_uuid (), + "userId" uuid NOT NULL, + "name" text NOT NULL, + "description" text NOT NULL, + "visibility" text NOT NULL +); + +ALTER TABLE ONLY "workspace" + ADD CONSTRAINT workspace_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY "workspace" + ADD CONSTRAINT workspace_fk_0 + FOREIGN KEY("userId") + REFERENCES trellouser(id) + ON DELETE CASCADE; + +-- +-- Class Member as table member +-- + +CREATE TABLE "member" ( + "id" uuid not null default gen_random_uuid (), + "workspaceId" uuid NOT NULL, + "userId" uuid NOT NULL, + "name" text NOT NULL, + "role" text NOT NULL +); + +ALTER TABLE ONLY "member" + ADD CONSTRAINT member_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY "member" + ADD CONSTRAINT member_fk_0 + FOREIGN KEY("workspaceId") + REFERENCES workspace(id) + ON DELETE CASCADE; +ALTER TABLE ONLY "member" + ADD CONSTRAINT member_fk_1 + FOREIGN KEY("userId") + REFERENCES trellouser(id) + ON DELETE CASCADE; + +-- +-- Class Board as table board +-- + +CREATE TABLE "board" ( + "id" uuid not null default gen_random_uuid (), + "workspaceId" uuid NOT NULL, + "userId" uuid NOT NULL, + "name" text NOT NULL, + "description" text, + "visibility" text NOT NULL, + "background" text NOT NULL, + "starred" boolean, + "enableCover" boolean, + "watch" boolean, + "availableOffline" boolean, + "label" text, + "emailAddress" text, + "commenting" integer, + "memberType" integer, + "pinned" boolean, + "selfJoin" boolean, + "close" boolean +); + +ALTER TABLE ONLY "board" + ADD CONSTRAINT board_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY "board" + ADD CONSTRAINT board_fk_0 + FOREIGN KEY("workspaceId") + REFERENCES workspace(id) + ON DELETE CASCADE; +ALTER TABLE ONLY "board" + ADD CONSTRAINT board_fk_1 + FOREIGN KEY("userId") + REFERENCES trellouser(id) + ON DELETE CASCADE; + +-- +-- Class Listboard as table listboard +-- + +CREATE TABLE "listboard" ( + "id" uuid not null default gen_random_uuid (), + "workspaceId" uuid NOT NULL, + "boardId" uuid NOT NULL, + "userId" uuid NOT NULL, + "name" text NOT NULL, + "archived" boolean, + "listOrder" integer +); + +ALTER TABLE ONLY "listboard" + ADD CONSTRAINT listboard_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY "listboard" + ADD CONSTRAINT listboard_fk_0 + FOREIGN KEY("boardId") + REFERENCES board(id) + ON DELETE CASCADE; +ALTER TABLE ONLY "listboard" + ADD CONSTRAINT listboard_fk_1 + FOREIGN KEY("userId") + REFERENCES trellouser(id) + ON DELETE CASCADE; + +-- +-- Class Cardlist as table card +-- + +CREATE TABLE "card" ( + "id" uuid not null default gen_random_uuid (), + "workspaceId" uuid NOT NULL, + "listId" uuid NOT NULL, + "userId" uuid NOT NULL, + "name" text NOT NULL, + "description" text, + "startDate" timestamp without time zone, + "dueDate" timestamp without time zone, + "rank" integer, + "attachment" boolean, + "archived" boolean, + "checklist" boolean, + "comments" boolean +); + +ALTER TABLE ONLY "card" + ADD CONSTRAINT card_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY "card" + ADD CONSTRAINT card_fk_0 + FOREIGN KEY("listId") + REFERENCES listboard(id) + ON DELETE CASCADE; +ALTER TABLE ONLY "card" + ADD CONSTRAINT card_fk_1 + FOREIGN KEY("userId") + REFERENCES trellouser(id) + ON DELETE CASCADE; + +-- +-- Class Attachment as table attachment +-- + +CREATE TABLE "attachment" ( + "id" uuid not null default gen_random_uuid (), + "workspaceId" uuid NOT NULL, + "userId" uuid NOT NULL, + "cardId" uuid NOT NULL, + "attachment" text NOT NULL +); + +ALTER TABLE ONLY "attachment" + ADD CONSTRAINT attachment_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY "attachment" + ADD CONSTRAINT attachment_fk_0 + FOREIGN KEY("userId") + REFERENCES trellouser(id) + ON DELETE CASCADE; +ALTER TABLE ONLY "attachment" + ADD CONSTRAINT attachment_fk_1 + FOREIGN KEY("cardId") + REFERENCES card(id) + ON DELETE CASCADE; + +-- +-- Class Checklist as table checklist +-- + +CREATE TABLE "checklist" ( + "id" uuid not null default gen_random_uuid (), + "workspaceId" uuid NOT NULL, + "cardId" uuid NOT NULL, + "name" text NOT NULL, + "status" boolean NOT NULL +); + +ALTER TABLE ONLY "checklist" + ADD CONSTRAINT checklist_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY "checklist" + ADD CONSTRAINT checklist_fk_0 + FOREIGN KEY("cardId") + REFERENCES card(id) + ON DELETE CASCADE; + +-- +-- Class Comment as table comment +-- + +CREATE TABLE "comment" ( + "id" uuid not null default gen_random_uuid (), + "workspaceId" uuid NOT NULL, + "cardId" uuid NOT NULL, + "userId" uuid NOT NULL, + "description" text NOT NULL +); + +ALTER TABLE ONLY "comment" + ADD CONSTRAINT comment_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY "comment" + ADD CONSTRAINT comment_fk_0 + FOREIGN KEY("cardId") + REFERENCES card(id) + ON DELETE CASCADE; +ALTER TABLE ONLY "comment" + ADD CONSTRAINT comment_fk_1 + FOREIGN KEY("userId") + REFERENCES trellouser(id) + ON DELETE CASCADE; + +-- +-- Class Activity as table activity +-- + +CREATE TABLE "activity" ( + "id" uuid not null default gen_random_uuid (), + "workspaceId" uuid NOT NULL, + "boardId" uuid, + "userId" uuid NOT NULL, + "cardId" uuid, + "description" text NOT NULL, + "dateCreated" timestamp without time zone NOT NULL +); + +ALTER TABLE ONLY "activity" + ADD CONSTRAINT activity_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY "activity" + ADD CONSTRAINT activity_fk_0 + FOREIGN KEY("boardId") + REFERENCES board(id) + ON DELETE CASCADE; +ALTER TABLE ONLY "activity" + ADD CONSTRAINT activity_fk_1 + FOREIGN KEY("userId") + REFERENCES trellouser(id) + ON DELETE CASCADE; +ALTER TABLE ONLY "activity" + ADD CONSTRAINT activity_fk_2 + FOREIGN KEY("cardId") + REFERENCES card(id) + ON DELETE CASCADE; + +-- +-- Class Board Labels as table board_labels +-- + +CREATE TABLE "board_label" ( + "id" uuid not null default gen_random_uuid (), + "boardId" uuid NOT NULL, + "workspaceId" uuid NOT NULL, + "title" text NOT NULL, + "color" text NOT NULL, + "dateCreated" timestamp without time zone NOT NULL +); + +ALTER TABLE ONLY "board_label" + ADD CONSTRAINT board_label_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY "board_label" + ADD CONSTRAINT board_label_fk_0 + FOREIGN KEY("boardId") + REFERENCES board(id) + ON DELETE CASCADE; +ALTER TABLE ONLY "board_label" + ADD CONSTRAINT board_label_fk_1 + FOREIGN KEY("workspaceId") + REFERENCES workspace(id) + ON DELETE CASCADE; + +-- +-- Class Card Labels as table card_labels +-- + +CREATE TABLE "card_label" ( + "id" uuid not null default gen_random_uuid (), + "boardLabelId" uuid NOT NULL, + "workspaceId" uuid NOT NULL, + "boardId" uuid NOT NULL, + "cardId" uuid NOT NULL, + "dateCreated" timestamp without time zone NOT NULL +); + +ALTER TABLE ONLY "card_label" + ADD CONSTRAINT card_label_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY "card_label" + ADD CONSTRAINT card_label_fk_0 + FOREIGN KEY("boardLabelId") + REFERENCES board_label(id) + ON DELETE CASCADE; +ALTER TABLE ONLY "card_label" + ADD CONSTRAINT card_label_fk_1 + FOREIGN KEY("cardId") + REFERENCES card(id) + ON DELETE CASCADE; +ALTER TABLE ONLY "card_label" + ADD CONSTRAINT card_label_fk_2 + FOREIGN KEY("workspaceId") + REFERENCES workspace(id) + ON DELETE CASCADE; +ALTER TABLE ONLY "card_label" + ADD CONSTRAINT card_label_fk_3 + FOREIGN KEY("boardId") + REFERENCES board(id) + ON DELETE CASCADE; \ No newline at end of file diff --git a/demos/supabase-trello/web/favicon.png b/demos/supabase-trello/web/favicon.png new file mode 100644 index 00000000..8aaa46ac Binary files /dev/null and b/demos/supabase-trello/web/favicon.png differ diff --git a/demos/supabase-trello/web/icons/Icon-192.png b/demos/supabase-trello/web/icons/Icon-192.png new file mode 100644 index 00000000..b749bfef Binary files /dev/null and b/demos/supabase-trello/web/icons/Icon-192.png differ diff --git a/demos/supabase-trello/web/icons/Icon-512.png b/demos/supabase-trello/web/icons/Icon-512.png new file mode 100644 index 00000000..88cfd48d Binary files /dev/null and b/demos/supabase-trello/web/icons/Icon-512.png differ diff --git a/demos/supabase-trello/web/icons/Icon-maskable-192.png b/demos/supabase-trello/web/icons/Icon-maskable-192.png new file mode 100644 index 00000000..eb9b4d76 Binary files /dev/null and b/demos/supabase-trello/web/icons/Icon-maskable-192.png differ diff --git a/demos/supabase-trello/web/icons/Icon-maskable-512.png b/demos/supabase-trello/web/icons/Icon-maskable-512.png new file mode 100644 index 00000000..d69c5669 Binary files /dev/null and b/demos/supabase-trello/web/icons/Icon-maskable-512.png differ diff --git a/demos/supabase-trello/web/index.html b/demos/supabase-trello/web/index.html new file mode 100644 index 00000000..3b048eea --- /dev/null +++ b/demos/supabase-trello/web/index.html @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + trelloappclone_flutter + + + + + + + + + + diff --git a/demos/supabase-trello/web/manifest.json b/demos/supabase-trello/web/manifest.json new file mode 100644 index 00000000..a2531a89 --- /dev/null +++ b/demos/supabase-trello/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "trelloappclone_flutter", + "short_name": "trelloappclone_flutter", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/demos/supabase-trello/windows/.gitignore b/demos/supabase-trello/windows/.gitignore new file mode 100644 index 00000000..d492d0d9 --- /dev/null +++ b/demos/supabase-trello/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/demos/supabase-trello/windows/CMakeLists.txt b/demos/supabase-trello/windows/CMakeLists.txt new file mode 100644 index 00000000..c5b3a9b4 --- /dev/null +++ b/demos/supabase-trello/windows/CMakeLists.txt @@ -0,0 +1,102 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(trelloappclone_flutter LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "trelloappclone_flutter") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/demos/supabase-trello/windows/flutter/CMakeLists.txt b/demos/supabase-trello/windows/flutter/CMakeLists.txt new file mode 100644 index 00000000..930d2071 --- /dev/null +++ b/demos/supabase-trello/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/demos/supabase-trello/windows/flutter/generated_plugin_registrant.cc b/demos/supabase-trello/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..2a044a64 --- /dev/null +++ b/demos/supabase-trello/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,26 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + AppLinksPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksPluginCApi")); + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); + PowersyncFlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PowersyncFlutterLibsPlugin")); + Sqlite3FlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/demos/supabase-trello/windows/flutter/generated_plugin_registrant.h b/demos/supabase-trello/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..dc139d85 --- /dev/null +++ b/demos/supabase-trello/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/demos/supabase-trello/windows/flutter/generated_plugins.cmake b/demos/supabase-trello/windows/flutter/generated_plugins.cmake new file mode 100644 index 00000000..18c83191 --- /dev/null +++ b/demos/supabase-trello/windows/flutter/generated_plugins.cmake @@ -0,0 +1,28 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + app_links + file_selector_windows + powersync_flutter_libs + sqlite3_flutter_libs + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/demos/supabase-trello/windows/runner/CMakeLists.txt b/demos/supabase-trello/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..394917c0 --- /dev/null +++ b/demos/supabase-trello/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/demos/supabase-trello/windows/runner/Runner.rc b/demos/supabase-trello/windows/runner/Runner.rc new file mode 100644 index 00000000..58312f4e --- /dev/null +++ b/demos/supabase-trello/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "trelloappclone_flutter" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "trelloappclone_flutter" "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "trelloappclone_flutter.exe" "\0" + VALUE "ProductName", "trelloappclone_flutter" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/demos/supabase-trello/windows/runner/flutter_window.cpp b/demos/supabase-trello/windows/runner/flutter_window.cpp new file mode 100644 index 00000000..b25e363e --- /dev/null +++ b/demos/supabase-trello/windows/runner/flutter_window.cpp @@ -0,0 +1,66 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/demos/supabase-trello/windows/runner/flutter_window.h b/demos/supabase-trello/windows/runner/flutter_window.h new file mode 100644 index 00000000..6da0652f --- /dev/null +++ b/demos/supabase-trello/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/demos/supabase-trello/windows/runner/main.cpp b/demos/supabase-trello/windows/runner/main.cpp new file mode 100644 index 00000000..8dfa412e --- /dev/null +++ b/demos/supabase-trello/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"trelloappclone_flutter", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/demos/supabase-trello/windows/runner/resource.h b/demos/supabase-trello/windows/runner/resource.h new file mode 100644 index 00000000..66a65d1e --- /dev/null +++ b/demos/supabase-trello/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/demos/supabase-trello/windows/runner/resources/app_icon.ico b/demos/supabase-trello/windows/runner/resources/app_icon.ico new file mode 100644 index 00000000..c04e20ca Binary files /dev/null and b/demos/supabase-trello/windows/runner/resources/app_icon.ico differ diff --git a/demos/supabase-trello/windows/runner/runner.exe.manifest b/demos/supabase-trello/windows/runner/runner.exe.manifest new file mode 100644 index 00000000..a42ea768 --- /dev/null +++ b/demos/supabase-trello/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/demos/supabase-trello/windows/runner/utils.cpp b/demos/supabase-trello/windows/runner/utils.cpp new file mode 100644 index 00000000..b2b08734 --- /dev/null +++ b/demos/supabase-trello/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length <= 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/demos/supabase-trello/windows/runner/utils.h b/demos/supabase-trello/windows/runner/utils.h new file mode 100644 index 00000000..3879d547 --- /dev/null +++ b/demos/supabase-trello/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/demos/supabase-trello/windows/runner/win32_window.cpp b/demos/supabase-trello/windows/runner/win32_window.cpp new file mode 100644 index 00000000..60608d0f --- /dev/null +++ b/demos/supabase-trello/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/demos/supabase-trello/windows/runner/win32_window.h b/demos/supabase-trello/windows/runner/win32_window.h new file mode 100644 index 00000000..e901dde6 --- /dev/null +++ b/demos/supabase-trello/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/docs/update_core.md b/docs/update_core.md new file mode 100644 index 00000000..0916fb86 --- /dev/null +++ b/docs/update_core.md @@ -0,0 +1,10 @@ +To update the version of the PowerSync core extension used in the Dart SDK, update the following locations: + +1. `scripts/download_core_binary_demos.dart` and `scripts/init_powersync_core_binary.dart`. +2. `build.gradle` for `powersync_flutter_libs`. +3. `powersync_flutter_libs` (iOS and macOS) for `powersync_flutter_libs`. +4. `POWERSYNC_CORE_VERSION` in `sqlite3_wasm_build/build.sh`. + +After updating, run `podfile:update` to update the podfile locks for demo projects. +If you've updated the core version to use a new feature, also update the minimum +version in `core_version.dart` to reflect that requirement. diff --git a/melos.yaml b/melos.yaml index 1a04f0a1..f8f9a2ac 100644 --- a/melos.yaml +++ b/melos.yaml @@ -3,6 +3,7 @@ repository: https://github.com/powersync-ja/powersync.dart packages: - packages/* + - packages/powersync_sqlcipher/example - demos/* - demos/*/* @@ -18,7 +19,7 @@ command: # Automatically update libraryVersion: preCommit: | dart run tool/update_version.dart - git add packages/powersync/lib/src/version.dart + git add packages/powersync_core/lib/src/version.dart publish: hooks: pre: dart ./scripts/download_core_binary_demos.dart # Download latest core binaries when publishing @@ -39,10 +40,11 @@ scripts: packageFilters: private: true dirExists: web + dependsOn: powersync prepare:assets: description: Download Sqlite3 WASM for tests - run: dart ./bin/setup_web.dart --no-worker --output-dir assets + run: dart ./bin/setup_web.dart --no-worker --output-dir ../powersync_core/assets exec: concurrency: 1 packageFilters: @@ -74,6 +76,16 @@ scripts: description: Check formatting of Dart code in packages. run: dart format --output none --set-exit-if-changed packages + podfile:update: + description: Update Podfile.lock in demos. + run: cd ios && pod update && cd ../macos && pod update + exec: + concurrency: 1 + packageFilters: + dirExists: + - ios + - macos + test: description: Run tests in a specific package. run: flutter test @@ -82,6 +94,7 @@ scripts: packageFilters: dirExists: - test + dependsOn: test # This tells Melos tests to ignore env variables passed to tests from `melos run test` # as they could change the behaviour of how tests filter packages. env: @@ -93,6 +106,7 @@ scripts: exec: concurrency: 1 packageFilters: + dependsOn: test dirExists: - test env: diff --git a/packages/powersync/CHANGELOG.md b/packages/powersync/CHANGELOG.md index 43b2adf0..4f30cb92 100644 --- a/packages/powersync/CHANGELOG.md +++ b/packages/powersync/CHANGELOG.md @@ -1,10 +1,129 @@ -## 1.9.3 +## 1.16.1 + + - Web: Fix decoding sync streams on status. + +## 1.16.0 + +- Add `getCrudTransactions()` returning a stream of completed transactions for uploads. +- Add experimental support for [sync streams](https://docs.powersync.com/usage/sync-streams). +- Add new attachments helper implementation in `package:powersync_core/attachments/attachments.dart`. +- Add SwiftPM support. + +## 1.15.2 + + - Fix excessive memory consumption during large sync. + +## 1.15.1 + + - Support latest versions of `package:sqlite3` and `package:sqlite_async`. + - Stream client: Improve `disconnect()` while a connection is being opened. + - Stream client: Support binary sync lines with Rust client and compatible PowerSync service versions. + - Sync client: Improve parsing error responses. + +## 1.15.0 + + - Update the PowerSync core extension to `0.4.2`. + - Add support for [raw tables](https://docs.powersync.com/usage/use-case-examples/raw-tables), which are user-managed + regular SQLite tables instead of the JSON-based views managed by PowerSync. + +## 1.14.1 + + - Rust client: Fix uploading local writs after reconnect. + - `PowerSyncDatabase.withDatabase`: Rename `loggers` parameter to `logger` for consistency. + - Fix parsing HTTP errors for sync service unavailability. + +## 1.14.0 + +Add a new sync client implementation written in Rust instead of Dart. While +this client is still experimental, we intend to make it the default in the +future. The main benefit of this client is faster sync performance, but +upcoming features will also require this client. +We encourage interested users to try it out by passing `SyncOptions` to the +`connect` method: + +```dart +database.connect( + connector: YourConnector(), + options: const SyncOptions( + syncImplementation: SyncClientImplementation.rust, + ), +); +``` + +Switching between the clients can be done at any time without compatibility +issues. If you run into issues with the new client, please reach out to us! + +## 1.13.1 + +- Use `package:http` instead of `package:fetch_client` on the web (since the former now uses fetch as well). +- Allow disconnecting in the credentials callback of a connector. +- Deprecate retry and CRUD upload durations as fields and independent parameters. Use the new `SyncOptions` class instead. +- Fix sync progress report after a compaction or defragmentation on the sync service. + +## 1.13.0 + +* Report real-time progress information about downloads through `SyncStatus.downloadProgress`. +* Add `trackPreviousValues` option on `Table` which sets `CrudEntry.previousValues` to previous values on updates. +* Add `trackMetadata` option on `Table` which adds a `_metadata` column that can be used for updates. + The configured metadata is available through `CrudEntry.metadata`. +* Add `ignoreEmptyUpdates` option which skips creating CRUD entries for updates that don't change any values. + +## 1.12.4 + + - Update a dependency to the latest release. + +## 1.12.3 + +This updates `powersync_core` to version `1.2.3`, which includes these changes: + + - Introduce locks to avoid duplicate sync streams when multiple instances of the same database are opened. + - Refactor connect / disconnect internally. + - Warn when multiple instances of the same database are opened. + - Fix race condition causing data not to be applied while an upload is in progress. + - Web: Fix token invalidation logic when a sync worker is used. + +## 1.12.2 + + - Update a dependency to the latest release. + +## 1.12.1 + + - Update a dependency to the latest release. + +## 1.12.0 + + - Support bucket priorities and partial syncs. + +## 1.11.3 - Update a dependency to the latest release. +## 1.11.2 + + - Web: Support running in contexts where web workers are unavailable. + - Web: Fix sync worker logs not being disabled. + - `powersync_sqlcipher`: Web support. + +## 1.11.1 + + - Fix `statusStream` emitting the same sync status multiple times. + +## 1.11.0 + + - Increase limit on number of columns per table to 1999. + - Avoid deleting the $local bucket on connect(). + +## 1.10.0 + +- This package now uses the `powersync_core` package to provide its base functionality. + +## 1.9.3 + +- Update a dependency to the latest release. + ## 1.9.2 - - [Web] Automatically flush IndexedDB storage to fix durability issues +- [Web] Automatically flush IndexedDB storage to fix durability issues ## 1.9.1 diff --git a/packages/powersync/README.md b/packages/powersync/README.md index e9fcf03d..472f153f 100644 --- a/packages/powersync/README.md +++ b/packages/powersync/README.md @@ -2,14 +2,20 @@

-# PowerSync SDK for Dart/Flutter +# PowerSync SDK for Flutter -*[PowerSync](https://www.powersync.com) is a sync engine for building local-first apps with instantly-responsive UI/UX and simplified state transfer. Syncs between SQLite on the client-side and Postgres, MongoDB or MySQL on the server-side.* +_[PowerSync](https://www.powersync.com) is a sync engine for building local-first apps with instantly-responsive UI/UX and simplified state transfer. Syncs between SQLite on the client-side and Postgres, MongoDB or MySQL on the server-side._ -This package (`powersync`) is the PowerSync client SDK for Dart/Flutter. +This package (`powersync`) is the PowerSync client SDK for Flutter. See a summary of features [here](https://docs.powersync.com/client-sdk-references/flutter). +> **Note** +> This package is recommended for Flutter applications that do not require on-device encryption. + +> - For Flutter applications that require encryption, use the [`powersync_sqlcipher`](https://pub.dev/packages/powersync_sqlcipher) package. +> - For use cases such as server-side Dart or non-Flutter Dart environments, use the [`powersync_core`](https://pub.dev/packages/powersync_core) package. + # Installation ```bash diff --git a/packages/powersync/analysis_options.yaml b/packages/powersync/analysis_options.yaml index dee8927a..4be9ff7b 100644 --- a/packages/powersync/analysis_options.yaml +++ b/packages/powersync/analysis_options.yaml @@ -13,6 +13,12 @@ include: package:lints/recommended.yaml +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + # Uncomment the following section to specify additional rules. # linter: diff --git a/packages/powersync/bin/setup_web.dart b/packages/powersync/bin/setup_web.dart index 6d29a6be..dd4042c6 100644 --- a/packages/powersync/bin/setup_web.dart +++ b/packages/powersync/bin/setup_web.dart @@ -1,176 +1,4 @@ -import 'dart:convert'; -import 'dart:io'; -import 'package:collection/collection.dart'; -import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; -import 'package:args/args.dart'; +// ignore: implementation_imports +import 'package:powersync_core/src/setup_web.dart'; -void main(List arguments) async { - var parser = ArgParser(); - // Add a flag to enable/disable the download of worker (defaults to true) - // Pass the --no-worker argument to disable the download of the worker - // dart run powersync:setup_web --no-worker - parser.addFlag('worker', defaultsTo: true); - // Add a option to specify the output directory (defaults to web) - // Pass the --output-dir argument to specify the output directory - // dart run powersync:setup_web --output-dir assets - parser.addOption('output-dir', abbr: 'o', defaultsTo: 'web'); - var results = parser.parse(arguments); - bool downloadWorker = results.flag('worker'); - String outputDir = results.option('output-dir')!; - - final root = Directory.current.uri; - print('Project root: ${root.toFilePath()}'); - - final wasmPath = '${root.toFilePath()}$outputDir/sqlite3.wasm'; - - final workerPath = '${root.toFilePath()}$outputDir/powersync_db.worker.js'; - final syncWorkerPath = - '${root.toFilePath()}$outputDir/powersync_sync.worker.js'; - - final packageConfigFile = File.fromUri( - root.resolve('.dart_tool/package_config.json'), - ); - dynamic packageConfig; - try { - packageConfig = json.decode(await packageConfigFile.readAsString()); - } on FileSystemException { - print('Missing .dart_tool/package_config.json'); - print('Run `flutter pub get` first.'); - exit(1); - } on FormatException { - print('Invalid .dart_tool/package_config.json'); - print('Run `flutter pub get` first.'); - exit(1); - } - - try { - final httpClient = HttpClient(); - - final powersyncPackageName = 'powersync'; - - if (downloadWorker) { - final powersyncPkg = - getPackageFromConfig(packageConfig, powersyncPackageName); - - final powersyncVersion = getPubspecVersion( - packageConfigFile, powersyncPkg, powersyncPackageName); - - final workerUrl = - 'https://github.com/powersync-ja/powersync.dart/releases/download/powersync-v$powersyncVersion/powersync_db.worker.js'; - - final syncWorkerUrl = - 'https://github.com/powersync-ja/powersync.dart/releases/download/powersync-v$powersyncVersion/powersync_sync.worker.js'; - - await downloadFile(httpClient, workerUrl, workerPath); - await downloadFile(httpClient, syncWorkerUrl, syncWorkerPath); - } - - final sqlitePackageName = 'sqlite3'; - - final sqlite3Pkg = getPackageFromConfig(packageConfig, sqlitePackageName); - - String sqlite3Version = - "v${getPubspecVersion(packageConfigFile, sqlite3Pkg, sqlitePackageName)}"; - - List tags = await getLatestTagsFromRelease(httpClient); - String? matchTag = tags.firstWhereOrNull((element) => - element.contains(sqlite3Version) && coreVersionIsInRange(element)); - if (matchTag != null) { - sqlite3Version = matchTag; - } else { - throw Exception( - """No compatible powersync core version found for sqlite3 version $sqlite3Version - Latest supported sqlite3 versions: ${tags.take(3).map((tag) => tag.split('-')[0]).join(', ')}. - You can view the full list of releases at https://github.com/powersync-ja/sqlite3.dart/releases"""); - } - - final sqliteUrl = - 'https://github.com/powersync-ja/sqlite3.dart/releases/download/$sqlite3Version/sqlite3.wasm'; - - await downloadFile(httpClient, sqliteUrl, wasmPath); - } catch (e) { - print(e); - exit(1); - } -} - -bool coreVersionIsInRange(String tag) { - // Sets the range of powersync core version that is compatible with the sqlite3 version - // We're a little more selective in the versions chosen here than the range - // we're compatible with. - VersionConstraint constraint = VersionConstraint.parse('>=0.3.0 <0.4.0'); - List parts = tag.split('-'); - String powersyncPart = parts[1]; - - List versionParts = powersyncPart.split('.'); - String extractedVersion = - versionParts.sublist(versionParts.length - 3).join('.'); - final coreVersion = Version.parse(extractedVersion); - if (constraint.allows(coreVersion)) { - return true; - } - return false; -} - -dynamic getPackageFromConfig(dynamic packageConfig, String packageName) { - final pkg = (packageConfig['packages'] ?? []).firstWhere( - (e) => e['name'] == packageName, - orElse: () => null, - ); - if (pkg == null) { - throw Exception('Dependency on package:$packageName is required'); - } - return pkg; -} - -String getPubspecVersion( - File packageConfigFile, dynamic package, String packageName) { - final rootUri = packageConfigFile.uri.resolve(package['rootUri'] ?? ''); - print('Using package:$packageName from ${rootUri.toFilePath()}'); - - String pubspec = - File('${rootUri.toFilePath()}/pubspec.yaml').readAsStringSync(); - Pubspec parsed = Pubspec.parse(pubspec); - final version = parsed.version?.toString(); - if (version == null) { - throw Exception( - "${capitalize(packageName)} version not found. Run `flutter pub get` first."); - } - return version; -} - -String capitalize(String s) => s[0].toUpperCase() + s.substring(1); - -Future> getLatestTagsFromRelease(HttpClient httpClient) async { - var request = await httpClient.getUrl(Uri.parse( - "https://api.github.com/repos/powersync-ja/sqlite3.dart/releases")); - var response = await request.close(); - if (response.statusCode == HttpStatus.ok) { - var res = await response.transform(utf8.decoder).join(); - List jsonObj = json.decode(res); - List tags = []; - for (dynamic obj in jsonObj) { - final tagName = obj['tag_name'] as String; - if (!tagName.contains("-powersync")) continue; - tags.add(tagName); - } - return tags; - } else { - throw Exception("Failed to fetch GitHub releases and tags"); - } -} - -Future downloadFile( - HttpClient httpClient, String url, String savePath) async { - print('Downloading: $url'); - var request = await httpClient.getUrl(Uri.parse(url)); - var response = await request.close(); - if (response.statusCode == HttpStatus.ok) { - var file = File(savePath); - await response.pipe(file.openWrite()); - } else { - throw Exception( - 'Failed to download file: ${response.statusCode} ${response.reasonPhrase}'); - } -} +void main(List args) => downloadWebAssets(args); diff --git a/packages/powersync/example/README.md b/packages/powersync/example/README.md index 26741f48..9db577ab 100644 --- a/packages/powersync/example/README.md +++ b/packages/powersync/example/README.md @@ -1,5 +1,5 @@ # Examples - * [Getting started](./getting_started.dart) - * [Watching queries](./watching_changes.dart) - * [Batch writes](./batch_writes.dart) +- [Getting started](./getting_started.dart) +- [Watching queries](./watching_changes.dart) +- [Batch writes](./batch_writes.dart) diff --git a/packages/powersync/example/batch_writes.dart b/packages/powersync/example/batch_writes.dart index 7691ec50..4a1dc2fe 100644 --- a/packages/powersync/example/batch_writes.dart +++ b/packages/powersync/example/batch_writes.dart @@ -9,12 +9,12 @@ const schema = Schema([ final parameterSets = List.generate(1000, (i) => [uuid.v4(), 'Row $i']); -openDatabase() async { +Future openDatabase() async { db = PowerSyncDatabase(schema: schema, path: 'test.db'); await db.initialize(); } -singleWrites() async { +Future singleWrites() async { // Execute each write as a separate statement. // Each write flushes the changes to persistent storage, so this is slow. for (var params in parameterSets) { @@ -22,7 +22,7 @@ singleWrites() async { } } -transactionalWrites() async { +Future transactionalWrites() async { // Combine all the writes into a single transaction, only flushing to // persistent storage once. await db.writeTransaction((tx) async { @@ -32,7 +32,7 @@ transactionalWrites() async { }); } -batchWrites() async { +Future batchWrites() async { // Combine all the writes into a single batch, automatically wrapped in a transaction. // This avoids the overhead of asynchronously waiting for each call to complete, // and also only parses the SQL statement once. @@ -40,7 +40,7 @@ batchWrites() async { 'INSERT INTO data(id, contents) VALUES(?, ?)', parameterSets); } -inIsolateWrites() async { +Future inIsolateWrites() async { // This is the same as executeBatch, but using the low-level sqlite APIs. // The call is executed in a transaction in the database isolate, with // synchronous access to the database. @@ -60,7 +60,7 @@ inIsolateWrites() async { }); } -main() async { +Future main() async { await openDatabase(); for (var call in [ singleWrites, diff --git a/packages/powersync/example/getting_started.dart b/packages/powersync/example/getting_started.dart index 348b3021..314860b8 100644 --- a/packages/powersync/example/getting_started.dart +++ b/packages/powersync/example/getting_started.dart @@ -25,7 +25,7 @@ class BackendConnector extends PowerSyncBackendConnector { } } -openDatabase() async { +Future openDatabase() async { var path = 'powersync-demo.db'; // getApplicationSupportDirectory is not supported on Web if (!kIsWeb) { diff --git a/packages/powersync/example/watching_changes.dart b/packages/powersync/example/watching_changes.dart index c36e41f2..74a9f34b 100644 --- a/packages/powersync/example/watching_changes.dart +++ b/packages/powersync/example/watching_changes.dart @@ -10,12 +10,12 @@ const schema = Schema([ final parameterSets = List.generate(1000, (i) => [uuid.v4(), 'Row $i']); -openDatabase() async { +Future openDatabase() async { db = PowerSyncDatabase(schema: schema, path: 'test.db'); await db.initialize(); } -main() async { +Future main() async { await openDatabase(); // Watch a single query. @@ -23,7 +23,7 @@ main() async { var subscription1 = db.watch('SELECT count() AS count FROM data').listen((results) { print('Results: $results'); - }, onError: (e) { + }, onError: (Object e) { print('Query failed: $e'); }); @@ -37,14 +37,14 @@ main() async { await db.get('SELECT sum(length(contents)) AS length FROM data'); print( 'Results after change to ${update.tables}: ${count['count']} entries, ${length['length']} characters'); - }).listen((_) {}, onError: (e) { + }).listen((_) {}, onError: (Object e) { print('Query failed: $e'); }); for (var i = 0; i < 10; i++) { await db.execute( 'INSERT INTO data(id, contents) VALUES(uuid(), ?)', ['Row $i']); - await Future.delayed(Duration(milliseconds: 500)); + await Future.delayed(Duration(milliseconds: 500)); } subscription1.cancel(); diff --git a/packages/powersync/lib/powersync.dart b/packages/powersync/lib/powersync.dart index d931d4f4..c5e05872 100644 --- a/packages/powersync/lib/powersync.dart +++ b/packages/powersync/lib/powersync.dart @@ -1,15 +1,6 @@ -/// PowerSync Dart SDK. +/// PowerSync Flutter SDK. /// /// Use [PowerSyncDatabase] to open a database. library; -export 'src/connector.dart'; -export 'src/crud.dart'; -export 'src/database/powersync_database.dart'; -export 'src/exceptions.dart'; -export 'src/log.dart'; -export 'src/open_factory.dart'; -export 'src/powersync_database.dart'; -export 'src/schema.dart'; -export 'src/sync_status.dart'; -export 'src/uuid.dart'; +export 'package:powersync_core/powersync_core.dart'; diff --git a/packages/powersync/lib/sqlite3.dart b/packages/powersync/lib/sqlite3.dart index 66f87ad4..2941247e 100644 --- a/packages/powersync/lib/sqlite3.dart +++ b/packages/powersync/lib/sqlite3.dart @@ -2,4 +2,4 @@ /// adding it as a direct dependency. library; -export 'package:sqlite_async/sqlite3.dart'; +export 'package:powersync_core/sqlite3.dart'; diff --git a/packages/powersync/lib/sqlite3_common.dart b/packages/powersync/lib/sqlite3_common.dart index df84a8e0..52887937 100644 --- a/packages/powersync/lib/sqlite3_common.dart +++ b/packages/powersync/lib/sqlite3_common.dart @@ -2,4 +2,4 @@ /// adding it as a direct dependency. library; -export 'package:sqlite_async/sqlite3_common.dart'; +export 'package:powersync_core/sqlite3_common.dart'; diff --git a/packages/powersync/lib/sqlite3_open.dart b/packages/powersync/lib/sqlite3_open.dart new file mode 100644 index 00000000..fd364dc4 --- /dev/null +++ b/packages/powersync/lib/sqlite3_open.dart @@ -0,0 +1,5 @@ +/// Re-exports [sqlite3_open](https://pub.dev/packages/sqlite3) to expose sqlite3_open without +/// adding it as a direct dependency. +library; + +export 'package:powersync_core/sqlite3_open.dart'; diff --git a/packages/powersync/lib/sqlite_async.dart b/packages/powersync/lib/sqlite_async.dart index 1d09936d..6deed835 100644 --- a/packages/powersync/lib/sqlite_async.dart +++ b/packages/powersync/lib/sqlite_async.dart @@ -2,4 +2,4 @@ /// adding it as a direct dependency. library; -export 'package:sqlite_async/sqlite_async.dart'; +export 'package:powersync_core/sqlite_async.dart'; diff --git a/packages/powersync/lib/src/database/powersync_db_mixin.dart b/packages/powersync/lib/src/database/powersync_db_mixin.dart deleted file mode 100644 index af8156e1..00000000 --- a/packages/powersync/lib/src/database/powersync_db_mixin.dart +++ /dev/null @@ -1,473 +0,0 @@ -import 'dart:async'; - -import 'package:logging/logging.dart'; -import 'package:meta/meta.dart'; -import 'package:powersync/sqlite3_common.dart'; -import 'package:powersync/sqlite_async.dart'; -import 'package:powersync/src/abort_controller.dart'; -import 'package:powersync/src/connector.dart'; -import 'package:powersync/src/crud.dart'; -import 'package:powersync/src/powersync_update_notification.dart'; -import 'package:powersync/src/schema.dart'; -import 'package:powersync/src/schema_logic.dart'; -import 'package:powersync/src/sync_status.dart'; - -mixin PowerSyncDatabaseMixin implements SqliteConnection { - /// Schema used for the local database. - Schema get schema; - - /// The underlying database. - /// - /// For the most part, behavior is the same whether querying on the underlying - /// database, or on [PowerSyncDatabase]. The main difference is in update notifications: - /// the underlying database reports updates to the underlying tables, while - /// [PowerSyncDatabase] reports updates to the higher-level views. - SqliteDatabase get database; - - /// The Logger used by this [PowerSyncDatabase]. - /// - /// The default is [autoLogger], which logs to the console in debug builds. - /// Use [debugLogger] to always log to the console. - /// Use [attachedLogger] to propagate logs to [Logger.root] for custom logging. - Logger get logger; - - Map? clientParams; - - /// Current connection status. - SyncStatus currentStatus = - const SyncStatus(connected: false, lastSyncedAt: null); - - /// Use this stream to subscribe to connection status updates. - late final Stream statusStream; - - @protected - StreamController statusStreamController = - StreamController.broadcast(); - - /// Use to prevent multiple connections from being opened concurrently - final Mutex _connectMutex = Mutex(); - - @override - - /// Broadcast stream that is notified of any table updates. - /// - /// Unlike in [SqliteDatabase.updates], the tables reported here are the - /// higher-level views as defined in the [Schema], and exclude the low-level - /// PowerSync tables. - late final Stream updates; - - /// Delay between retrying failed requests. - /// Defaults to 5 seconds. - /// Only has an effect if changed before calling [connect]. - Duration retryDelay = const Duration(seconds: 5); - - @protected - Future get isInitialized; - - /// null when disconnected, present when connecting or connected - @protected - AbortController? disconnecter; - - @protected - Future baseInit() async { - statusStream = statusStreamController.stream; - updates = powerSyncUpdateNotifications(database.updates); - - await database.initialize(); - await _checkVersion(); - await database.execute('SELECT powersync_init()'); - await updateSchema(schema); - await _updateHasSynced(); - } - - /// Check that a supported version of the powersync extension is loaded. - Future _checkVersion() async { - // Get version - String version; - try { - final row = - await database.get('SELECT powersync_rs_version() as version'); - version = row['version']; - } catch (e) { - throw SqliteException( - 1, 'The powersync extension is not loaded correctly. Details: $e'); - } - - // Parse version - List versionInts; - try { - versionInts = - version.split(RegExp(r'[./]')).take(3).map(int.parse).toList(); - } catch (e) { - throw SqliteException(1, - 'Unsupported powersync extension version. Need >=0.2.0 <1.0.0, got: $version. Details: $e'); - } - - // Validate >=0.2.0 <1.0.0 - if (versionInts[0] != 0 || - (versionInts[1] < 2) || - (versionInts[1] == 2 && versionInts[2] < 0)) { - throw SqliteException(1, - 'Unsupported powersync extension version. Need >=0.2.0 <1.0.0, got: $version'); - } - } - - /// Wait for initialization to complete. - /// - /// While initializing is automatic, this helps to catch and report initialization errors. - Future initialize() { - return isInitialized; - } - - Future _updateHasSynced() async { - // Query the database to see if any data has been synced. - final result = - await database.get('SELECT powersync_last_synced_at() as synced_at'); - final timestamp = result['synced_at'] as String?; - final hasSynced = timestamp != null; - - if (hasSynced != currentStatus.hasSynced) { - final lastSyncedAt = - timestamp == null ? null : DateTime.parse('${timestamp}Z').toLocal(); - final status = - SyncStatus(hasSynced: hasSynced, lastSyncedAt: lastSyncedAt); - setStatus(status); - } - } - - /// Returns a [Future] which will resolve once the first full sync has completed. - Future waitForFirstSync() async { - if (currentStatus.hasSynced ?? false) { - return; - } - await for (final result in statusStream) { - if (result.hasSynced ?? false) { - break; - } - } - } - - @protected - void setStatus(SyncStatus status) { - if (status != currentStatus) { - currentStatus = status.copyWith( - // Note that currently the streaming sync implementation will never set hasSynced. - // lastSyncedAt implies that syncing has completed at some point (hasSynced = true). - // The previous values of hasSynced should be preserved here. - hasSynced: status.lastSyncedAt != null - ? true - : status.hasSynced ?? currentStatus.hasSynced, - lastSyncedAt: status.lastSyncedAt ?? currentStatus.lastSyncedAt); - statusStreamController.add(currentStatus); - } - } - - @override - bool get closed { - return database.closed; - } - - /// Close the database, releasing resources. - /// - /// Also [disconnect]s any active connection. - /// - /// Once close is called, this connection cannot be used again - a new one - /// must be constructed. - @override - Future close() async { - // Don't close in the middle of the initialization process. - await isInitialized; - // Disconnect any active sync connection. - await disconnect(); - // Now we can close the database - await database.close(); - } - - /// Connect to the PowerSync service, and keep the databases in sync. - /// - /// The connection is automatically re-opened if it fails for any reason. - /// - /// Status changes are reported on [statusStream]. - Future connect( - {required PowerSyncBackendConnector connector, - - /// Throttle time between CRUD operations - /// Defaults to 10 milliseconds. - Duration crudThrottleTime = const Duration(milliseconds: 10), - Map? params}) async { - clientParams = params; - Zone current = Zone.current; - - Future reconnect() { - return _connectMutex.lock(() => baseConnect( - connector: connector, - crudThrottleTime: crudThrottleTime, - // The reconnect function needs to run in the original zone, - // to avoid recursive lock errors. - reconnect: current.bindCallback(reconnect), - params: params)); - } - - await reconnect(); - } - - /// Abstract connection method to be implemented by platform specific - /// classes. This is wrapped inside an exclusive mutex in the [connect] - /// method. - @protected - @internal - Future baseConnect( - {required PowerSyncBackendConnector connector, - - /// Throttle time between CRUD operations - /// Defaults to 10 milliseconds. - required Duration crudThrottleTime, - required Future Function() reconnect, - Map? params}); - - /// Close the sync connection. - /// - /// Use [connect] to connect again. - Future disconnect() async { - if (disconnecter != null) { - /// Checking `disconnecter.aborted` prevents race conditions - /// where multiple calls to `disconnect` can attempt to abort - /// the controller more than once before it has finished aborting. - if (disconnecter!.aborted == false) { - await disconnecter!.abort(); - disconnecter = null; - } else { - /// Wait for the abort to complete. Continue updating the sync status after completed - await disconnecter!.onAbort; - } - } - setStatus( - SyncStatus(connected: false, lastSyncedAt: currentStatus.lastSyncedAt)); - } - - /// Disconnect and clear the database. - /// - /// Use this when logging out. - /// - /// The database can still be queried after this is called, but the tables - /// would be empty. - /// - /// To preserve data in local-only tables, set [clearLocal] to false. - Future disconnectAndClear({bool clearLocal = true}) async { - await disconnect(); - - await writeTransaction((tx) async { - await tx.execute('select powersync_clear(?)', [clearLocal ? 1 : 0]); - }); - // The data has been deleted - reset these - currentStatus = SyncStatus(lastSyncedAt: null, hasSynced: false); - statusStreamController.add(currentStatus); - } - - @Deprecated('Use [disconnectAndClear] instead.') - Future disconnectedAndClear() async { - await disconnectAndClear(); - } - - /// Whether a connection to the PowerSync service is currently open. - bool get connected { - return currentStatus.connected; - } - - /// Replace the schema with a new version. - /// This is for advanced use cases - typically the schema should just be - /// specified once in the constructor. - /// - /// Cannot be used while connected - this should only be called before [connect]. - Future updateSchema(Schema schema); - - /// A connection factory that can be passed to different isolates. - /// - /// Use this to access the database in background isolates. - isolateConnectionFactory() { - return database.isolateConnectionFactory(); - } - - /// Get an unique id for this client. - /// This id is only reset when the database is deleted. - Future getClientId() async { - final row = await get('SELECT powersync_client_id() as client_id'); - return row['client_id'] as String; - } - - /// Get upload queue size estimate and count. - Future getUploadQueueStats( - {bool includeSize = false}) async { - if (includeSize) { - final row = await getOptional( - 'SELECT SUM(cast(data as blob) + 20) as size, count(*) as count FROM ps_crud'); - return UploadQueueStats( - count: row?['count'] ?? 0, size: row?['size'] ?? 0); - } else { - final row = await getOptional('SELECT count(*) as count FROM ps_crud'); - return UploadQueueStats(count: row?['count'] ?? 0); - } - } - - /// Get a batch of crud data to upload. - /// - /// Returns null if there is no data to upload. - /// - /// Use this from the [PowerSyncBackendConnector.uploadData]` callback. - /// - /// Once the data have been successfully uploaded, call [CrudBatch.complete] before - /// requesting the next batch. - /// - /// Use [limit] to specify the maximum number of updates to return in a single - /// batch. - /// - /// This method does include transaction ids in the result, but does not group - /// data by transaction. One batch may contain data from multiple transactions, - /// and a single transaction may be split over multiple batches. - Future getCrudBatch({limit = 100}) async { - final rows = await getAll( - 'SELECT id, tx_id, data FROM ps_crud ORDER BY id ASC LIMIT ?', - [limit + 1]); - List all = [for (var row in rows) CrudEntry.fromRow(row)]; - - var haveMore = false; - if (all.length > limit) { - all.removeLast(); - haveMore = true; - } - if (all.isEmpty) { - return null; - } - final last = all[all.length - 1]; - return CrudBatch( - crud: all, - haveMore: haveMore, - complete: ({String? writeCheckpoint}) async { - await writeTransaction((db) async { - await db - .execute('DELETE FROM ps_crud WHERE id <= ?', [last.clientId]); - if (writeCheckpoint != null && - await db.getOptional('SELECT 1 FROM ps_crud LIMIT 1') == null) { - await db.execute( - 'UPDATE ps_buckets SET target_op = CAST(? as INTEGER) WHERE name=\'\$local\'', - [writeCheckpoint]); - } else { - await db.execute( - 'UPDATE ps_buckets SET target_op = $maxOpId WHERE name=\'\$local\''); - } - }); - }); - } - - /// Get the next recorded transaction to upload. - /// - /// Returns null if there is no data to upload. - /// - /// Use this from the [PowerSyncBackendConnector.uploadData]` callback. - /// - /// Once the data have been successfully uploaded, call [CrudTransaction.complete] before - /// requesting the next transaction. - /// - /// Unlike [getCrudBatch], this only returns data from a single transaction at a time. - /// All data for the transaction is loaded into memory. - Future getNextCrudTransaction() async { - return await readTransaction((tx) async { - final first = await tx.getOptional( - 'SELECT id, tx_id, data FROM ps_crud ORDER BY id ASC LIMIT 1'); - if (first == null) { - return null; - } - final int? txId = first['tx_id']; - List all; - if (txId == null) { - all = [CrudEntry.fromRow(first)]; - } else { - final rows = await tx.getAll( - 'SELECT id, tx_id, data FROM ps_crud WHERE tx_id = ? ORDER BY id ASC', - [txId]); - all = [for (var row in rows) CrudEntry.fromRow(row)]; - } - - final last = all[all.length - 1]; - - return CrudTransaction( - transactionId: txId, - crud: all, - complete: ({String? writeCheckpoint}) async { - await writeTransaction((db) async { - await db.execute( - 'DELETE FROM ps_crud WHERE id <= ?', [last.clientId]); - if (writeCheckpoint != null && - await db.getOptional('SELECT 1 FROM ps_crud LIMIT 1') == - null) { - await db.execute( - 'UPDATE ps_buckets SET target_op = CAST(? as INTEGER) WHERE name=\'\$local\'', - [writeCheckpoint]); - } else { - await db.execute( - 'UPDATE ps_buckets SET target_op = $maxOpId WHERE name=\'\$local\''); - } - }); - }); - }); - } - - /// Takes a read lock, without starting a transaction. - /// - /// In most cases, [readTransaction] should be used instead. - @override - Future readLock(Future Function(SqliteReadContext tx) callback, - {String? debugContext, Duration? lockTimeout}); - - /// Takes a global lock, without starting a transaction. - /// - /// In most cases, [writeTransaction] should be used instead. - @override - Future writeLock(Future Function(SqliteWriteContext tx) callback, - {String? debugContext, Duration? lockTimeout}); - - @override - Stream watch(String sql, - {List parameters = const [], - Duration throttle = const Duration(milliseconds: 30), - Iterable? triggerOnTables}) { - if (triggerOnTables == null || triggerOnTables.isEmpty) { - return database.watch(sql, parameters: parameters, throttle: throttle); - } - List powersyncTables = []; - for (String tableName in triggerOnTables) { - powersyncTables.add(tableName); - powersyncTables.add(_prefixTableNames(tableName, 'ps_data__')); - powersyncTables.add(_prefixTableNames(tableName, 'ps_data_local__')); - } - return database.watch(sql, - parameters: parameters, - throttle: throttle, - triggerOnTables: powersyncTables); - } - - @protected - String _prefixTableNames(String tableName, String prefix) { - String prefixedString = tableName.replaceRange(0, 0, prefix); - return prefixedString; - } - - @override - Future getAutoCommit() { - return database.getAutoCommit(); - } - - @override - Future refreshSchema() async { - await database.refreshSchema(); - } -} - -Stream powerSyncUpdateNotifications( - Stream inner) { - return inner - .map((update) => - PowerSyncUpdateNotification.fromUpdateNotification(update)) - .where((update) => update.isNotEmpty) - .cast(); -} diff --git a/packages/powersync/lib/src/exceptions.dart b/packages/powersync/lib/src/exceptions.dart deleted file mode 100644 index d340dd60..00000000 --- a/packages/powersync/lib/src/exceptions.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'dart:async'; -import 'dart:convert' as convert; - -import 'package:http/http.dart' as http; - -/// This indicates an error with configured credentials. -class CredentialsException implements Exception { - String message; - - CredentialsException(this.message); - - @override - toString() { - return 'CredentialsException: $message'; - } -} - -/// An internal protocol exception. -/// -/// This indicates that the server sent an invalid response. -class PowerSyncProtocolException implements Exception { - String message; - - PowerSyncProtocolException(this.message); - - @override - toString() { - return 'SyncProtocolException: $message'; - } -} - -/// An error that received from the sync service. -/// -/// Examples include authorization errors (401) and temporary service issues (503). -class SyncResponseException implements Exception { - /// Parse an error response from the PowerSync service - static Future fromStreamedResponse( - http.StreamedResponse response) async { - try { - final body = await response.stream.bytesToString(); - final decoded = convert.jsonDecode(body); - final details = _stringOrFirst(decoded['error']?['details']) ?? body; - final message = '${response.reasonPhrase ?? "Request failed"}: $details'; - return SyncResponseException(response.statusCode, message); - } on Error catch (_) { - return SyncResponseException( - response.statusCode, - response.reasonPhrase ?? "Request failed", - ); - } - } - - /// Parse an error response from the PowerSync service - static SyncResponseException fromResponse(http.Response response) { - try { - final body = response.body; - final decoded = convert.jsonDecode(body); - final details = _stringOrFirst(decoded['error']?['details']) ?? body; - final message = '${response.reasonPhrase ?? "Request failed"}: $details'; - return SyncResponseException(response.statusCode, message); - } on Error catch (_) { - return SyncResponseException( - response.statusCode, - response.reasonPhrase ?? "Request failed", - ); - } - } - - int statusCode; - String description; - - SyncResponseException(this.statusCode, this.description); - - @override - toString() { - return 'SyncResponseException: $statusCode $description'; - } -} - -String? _stringOrFirst(Object? details) { - if (details == null) { - return null; - } else if (details is String) { - return details; - } else if (details is List && details[0] is String) { - return details[0]; - } else { - return null; - } -} - -class PowersyncNotReadyException implements Exception { - /// @nodoc - PowersyncNotReadyException(this.message); - - final String message; -} diff --git a/packages/powersync/lib/src/powersync_database.dart b/packages/powersync/lib/src/powersync_database.dart deleted file mode 100644 index c051b90d..00000000 --- a/packages/powersync/lib/src/powersync_database.dart +++ /dev/null @@ -1 +0,0 @@ -export 'package:powersync/src/database/powersync_database.dart'; diff --git a/packages/powersync/lib/src/schema.dart b/packages/powersync/lib/src/schema.dart deleted file mode 100644 index efa87abc..00000000 --- a/packages/powersync/lib/src/schema.dart +++ /dev/null @@ -1,265 +0,0 @@ -import 'schema_logic.dart'; - -/// The schema used by the database. -/// -/// The implementation uses the schema as a "VIEW" on top of JSON data. -/// No migrations are required on the client. -class Schema { - /// List of tables in the schema. - final List tables; - - const Schema(this.tables); - - Map toJson() => {'tables': tables}; - - void validate() { - Set tableNames = {}; - for (var table in tables) { - table.validate(); - - if (tableNames.contains(table.name)) { - throw AssertionError("Duplicate table name: ${table.name}"); - } - - tableNames.add(table.name); - } - } -} - -/// A single table in the schema. -class Table { - /// The synced table name, matching sync rules. - final String name; - - /// List of columns. - final List columns; - - /// List of indexes. - final List indexes; - - /// Whether the table only exists only. - final bool localOnly; - - /// Whether this is an insert-only table. - final bool insertOnly; - - /// Override the name for the view - final String? _viewNameOverride; - - /// There is maximum of 127 arguments for any function in SQLite. Currently we use json_object which uses 1 arg per key (column name) - /// and one per value, which limits it to 63 arguments. - final int maxNumberOfColumns = 63; - - /// Internal use only. - /// - /// Name of the table that stores the underlying data. - String get internalName { - if (localOnly) { - return "ps_data_local__$name"; - } else { - return "ps_data__$name"; - } - } - - /// Create a synced table. - /// - /// Local changes are recorded, and remote changes are synced to the local table. - const Table(this.name, this.columns, - {this.indexes = const [], String? viewName, this.localOnly = false}) - : insertOnly = false, - _viewNameOverride = viewName; - - /// Create a table that only exists locally. - /// - /// This table does not record changes, and is not synchronized from the service. - const Table.localOnly(this.name, this.columns, - {this.indexes = const [], String? viewName}) - : localOnly = true, - insertOnly = false, - _viewNameOverride = viewName; - - /// Create a table that only supports inserts. - /// - /// This table records INSERT statements, but does not persist data locally. - /// - /// SELECT queries on the table will always return 0 rows. - const Table.insertOnly(this.name, this.columns, {String? viewName}) - : localOnly = false, - insertOnly = true, - indexes = const [], - _viewNameOverride = viewName; - - Column operator [](String columnName) { - return columns.firstWhere((element) => element.name == columnName); - } - - bool get validName { - return !invalidSqliteCharacters.hasMatch(name) && - (_viewNameOverride == null || - !invalidSqliteCharacters.hasMatch(_viewNameOverride)); - } - - /// Check that there are no issues in the table definition. - void validate() { - if (columns.length > maxNumberOfColumns) { - throw AssertionError( - "Table $name has more than $maxNumberOfColumns columns, which is not supported"); - } - - if (invalidSqliteCharacters.hasMatch(name)) { - throw AssertionError("Invalid characters in table name: $name"); - } - - if (_viewNameOverride != null && - invalidSqliteCharacters.hasMatch(_viewNameOverride)) { - throw AssertionError( - "Invalid characters in view name: $_viewNameOverride"); - } - - Set columnNames = {"id"}; - for (var column in columns) { - if (column.name == 'id') { - throw AssertionError( - "$name: id column is automatically added, custom id columns are not supported"); - } else if (columnNames.contains(column.name)) { - throw AssertionError("Duplicate column $name.${column.name}"); - } else if (invalidSqliteCharacters.hasMatch(column.name)) { - throw AssertionError( - "Invalid characters in column name: $name.${column.name}"); - } - - columnNames.add(column.name); - } - Set indexNames = {}; - - for (var index in indexes) { - if (indexNames.contains(index.name)) { - throw AssertionError("Duplicate index $name.${index.name}"); - } else if (invalidSqliteCharacters.hasMatch(index.name)) { - throw AssertionError( - "Invalid characters in index name: $name.${index.name}"); - } - - for (var column in index.columns) { - if (!columnNames.contains(column.column)) { - throw AssertionError( - "Column $name.${column.column} not found for index ${index.name}"); - } - } - - indexNames.add(index.name); - } - } - - /// Name for the view, used for queries. - /// Defaults to the synced table name. - String get viewName { - return _viewNameOverride ?? name; - } - - Map toJson() => { - 'name': name, - 'view_name': _viewNameOverride, - 'local_only': localOnly, - 'insert_only': insertOnly, - 'columns': columns, - 'indexes': indexes.map((e) => e.toJson(this)).toList(growable: false) - }; -} - -class Index { - /// Descriptive name of the index. - final String name; - - /// List of columns used for the index. - final List columns; - - /// Construct a new index with the specified columns. - const Index(this.name, this.columns); - - /// Construct a new index with the specified column names. - factory Index.ascending(String name, List columns) { - return Index(name, - columns.map((e) => IndexedColumn.ascending(e)).toList(growable: false)); - } - - /// Internal use only. - /// - /// Specifies the full name of this index on a table. - String fullName(Table table) { - return "${table.internalName}__$name"; - } - - Map toJson(Table table) => { - 'name': name, - 'columns': columns.map((c) => c.toJson(table)).toList(growable: false) - }; -} - -/// Describes an indexed column. -class IndexedColumn { - /// Name of the column to index. - final String column; - - /// Whether this column is stored in ascending order in the index. - final bool ascending; - - const IndexedColumn(this.column, {this.ascending = true}); - const IndexedColumn.ascending(this.column) : ascending = true; - const IndexedColumn.descending(this.column) : ascending = false; - - Map toJson(Table table) { - final t = table[column].type; - - return {'name': column, 'ascending': ascending, 'type': t.sqlite}; - } -} - -/// A single column in a table schema. -class Column { - /// Name of the column. - final String name; - - /// Type of the column. - /// - /// If the underlying data does not match this type, - /// it is cast automatically. - /// - /// For details on the cast, see: - /// https://www.sqlite.org/lang_expr.html#castexpr - final ColumnType type; - - const Column(this.name, this.type); - - /// Create a TEXT column. - const Column.text(this.name) : type = ColumnType.text; - - /// Create an INTEGER column. - const Column.integer(this.name) : type = ColumnType.integer; - - /// Create a REAL column. - const Column.real(this.name) : type = ColumnType.real; - - Map toJson() => {'name': name, 'type': type.sqlite}; -} - -/// Type of column. -enum ColumnType { - /// TEXT column. - text('TEXT'), - - /// INTEGER column. - integer('INTEGER'), - - /// REAL column. - real('REAL'); - - final String sqlite; - - const ColumnType(this.sqlite); - - @override - toString() { - return sqlite; - } -} diff --git a/packages/powersync/lib/src/stream_utils.dart b/packages/powersync/lib/src/stream_utils.dart deleted file mode 100644 index 37aee207..00000000 --- a/packages/powersync/lib/src/stream_utils.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'dart:async'; - -import 'package:http/http.dart'; -import 'dart:convert' as convert; - -/// Inject a broadcast stream into a normal stream. -Stream addBroadcast(Stream a, Stream broadcast) { - return mergeStreams([a, broadcast]); -} - -/// This is similar in functionality to rxdart's MergeStream. -/// The resulting stream emits values from either stream, as soon as they are -/// received. -/// -/// One difference is that if _any_ of the streams are closed, the resulting -/// stream is closed. -Stream mergeStreams(List> streams) { - final controller = StreamController(sync: true); - - List>? subscriptions; - - controller.onListen = () { - subscriptions = streams.map((stream) { - return stream.listen((event) { - return controller.add(event); - }, onDone: () { - controller.close(); - }, onError: controller.addError); - }).toList(); - }; - - controller.onCancel = () { - if (subscriptions != null) { - // Important: The Future must be returned here. - // Since calling cancel on one of the subscriptions may error, - // not returning the Future may result in an unhandled error. - return cancelAll(subscriptions!); - } - }; - - controller.onPause = () { - if (subscriptions != null) { - return pauseAll(subscriptions!); - } - }; - - controller.onResume = () { - if (subscriptions != null) { - return resumeAll(subscriptions!); - } - }; - - return controller.stream; -} - -/// Given a raw ByteStream, parse each line as JSON. -Stream ndjson(ByteStream input) { - final textInput = input.transform(convert.utf8.decoder); - final lineInput = textInput.transform(const convert.LineSplitter()); - final jsonInput = lineInput.transform( - StreamTransformer.fromHandlers(handleError: (error, stackTrace, sink) { - /// On Web if the connection is closed, this error will throw, but - /// the stream is never closed. This closes the stream on error. - sink.close(); - }, handleData: (String data, EventSink sink) { - sink.add(convert.jsonDecode(data)); - })); - return jsonInput; -} - -/// Given a raw ByteStream, parse each line as JSON. -Stream newlines(ByteStream input) { - final textInput = input.transform(convert.utf8.decoder); - final lineInput = textInput.transform(const convert.LineSplitter()); - return lineInput; -} - -void pauseAll(List subscriptions) { - for (var sub in subscriptions) { - sub.pause(); - } -} - -void resumeAll(List subscriptions) { - for (var sub in subscriptions) { - sub.resume(); - } -} - -Future cancelAll(List subscriptions) async { - final futures = subscriptions.map((sub) => sub.cancel()); - await Future.wait(futures); -} diff --git a/packages/powersync/lib/src/streaming_sync.dart b/packages/powersync/lib/src/streaming_sync.dart deleted file mode 100644 index 27c1bd62..00000000 --- a/packages/powersync/lib/src/streaming_sync.dart +++ /dev/null @@ -1,534 +0,0 @@ -import 'dart:async'; -import 'dart:convert' as convert; - -import 'package:http/http.dart' as http; -import 'package:powersync/src/abort_controller.dart'; -import 'package:powersync/src/exceptions.dart'; -import 'package:powersync/src/log_internal.dart'; -import 'package:powersync/src/user_agent/user_agent.dart'; -import 'package:sqlite_async/mutex.dart'; - -import 'bucket_storage.dart'; -import 'connector.dart'; -import 'crud.dart'; -import 'stream_utils.dart'; -import 'sync_status.dart'; -import 'sync_types.dart'; - -/// Since we use null to indicate "no change" in status updates, we need -/// a different value to indicate "no error". -const _noError = Object(); - -abstract interface class StreamingSync { - Stream get statusStream; - - Future streamingSync(); - - /// Close any active streams. - Future abort(); -} - -class StreamingSyncImplementation implements StreamingSync { - BucketStorage adapter; - - final Future Function() credentialsCallback; - final Future Function()? invalidCredentialsCallback; - - final Future Function() uploadCrud; - - // An internal controller which is used to trigger CRUD uploads internally - // e.g. when reconnecting. - // This is only a broadcast controller since the `crudLoop` method is public - // and could potentially be called multiple times externally. - final StreamController _internalCrudTriggerController = - StreamController.broadcast(); - - final Stream crudUpdateTriggerStream; - - final StreamController _statusStreamController = - StreamController.broadcast(); - - @override - late final Stream statusStream; - - late final http.Client _client; - - final StreamController _localPingController = - StreamController.broadcast(); - - final Duration retryDelay; - - final Map? syncParameters; - - SyncStatus lastStatus = const SyncStatus(); - - AbortController? _abort; - - bool _safeToClose = true; - - final Mutex syncMutex, crudMutex; - - final Map _userAgentHeaders; - - String? clientId; - - StreamingSyncImplementation( - {required this.adapter, - required this.credentialsCallback, - this.invalidCredentialsCallback, - required this.uploadCrud, - required this.crudUpdateTriggerStream, - required this.retryDelay, - this.syncParameters, - required http.Client client, - - /// A unique identifier for this streaming sync implementation - /// A good value is typically the DB file path which it will mutate when syncing. - String? identifier = "unknown"}) - : syncMutex = Mutex(identifier: "sync-$identifier"), - crudMutex = Mutex(identifier: "crud-$identifier"), - _userAgentHeaders = userAgentHeaders() { - _client = client; - statusStream = _statusStreamController.stream; - } - - @override - Future abort() async { - // If streamingSync() hasn't been called yet, _abort will be null. - var future = _abort?.abort(); - // This immediately triggers a new iteration in the merged stream, allowing us - // to break immediately. - // However, we still need to close the underlying stream explicitly, otherwise - // the break will wait for the next line of data received on the stream. - _localPingController.add(null); - // According to the documentation, the behavior is undefined when calling - // close() while requests are pending. However, this is no other - // known way to cancel open streams, and this appears to end the stream with - // a consistent ClientException if a request is open. - // We avoid closing the client while opening a request, as that does cause - // unpredicable uncaught errors. - if (_safeToClose) { - _client.close(); - } - - await _internalCrudTriggerController.close(); - - // wait for completeAbort() to be called - await future; - - // Now close the client in all cases not covered above - _client.close(); - } - - bool get aborted { - return _abort?.aborted ?? false; - } - - bool get isConnected { - return lastStatus.connected; - } - - @override - Future streamingSync() async { - try { - _abort = AbortController(); - clientId = await adapter.getClientId(); - crudLoop(); - var invalidCredentials = false; - while (!aborted) { - _updateStatus(connecting: true); - try { - if (invalidCredentials && invalidCredentialsCallback != null) { - // This may error. In that case it will be retried again on the next - // iteration. - await invalidCredentialsCallback!(); - invalidCredentials = false; - } - // Protect sync iterations with exclusivity (if a valid Mutex is provided) - await syncMutex.lock( - () => streamingSyncIteration(abortController: _abort), - timeout: retryDelay); - } catch (e, stacktrace) { - if (aborted && e is http.ClientException) { - // Explicit abort requested - ignore. Example error: - // ClientException: Connection closed while receiving data, uri=http://localhost:8080/sync/stream - return; - } - final message = _syncErrorMessage(e); - isolateLogger.warning('Sync error: $message', e, stacktrace); - invalidCredentials = true; - - _updateStatus( - connected: false, - connecting: true, - downloading: false, - downloadError: e); - - // On error, wait a little before retrying - // When aborting, don't wait - await _delayRetry(); - } - } - } finally { - _abort!.completeAbort(); - } - } - - Future crudLoop() async { - await uploadAllCrud(); - - // Trigger a CRUD upload whenever the upstream trigger fires - // as-well-as whenever the sync stream reconnects. - // This has the potential (in rare cases) to affect the crudThrottleTime, - // but it should not result in excessive uploads since the - // sync reconnects are also throttled. - // The stream here is closed on abort. - await for (var _ in mergeStreams( - [crudUpdateTriggerStream, _internalCrudTriggerController.stream])) { - await uploadAllCrud(); - } - } - - Future uploadAllCrud() async { - return crudMutex.lock(() async { - // Keep track of the first item in the CRUD queue for the last `uploadCrud` iteration. - CrudEntry? checkedCrudItem; - - while (true) { - try { - // It's possible that an abort or disconnect operation could - // be followed by a `close` operation. The close would cause these - // operations, which use the DB, to throw an exception. Breaking the loop - // here prevents unnecessary potential (caught) exceptions. - if (aborted) { - break; - } - // This is the first item in the FIFO CRUD queue. - CrudEntry? nextCrudItem = await adapter.nextCrudItem(); - if (nextCrudItem != null) { - _updateStatus(uploading: true); - if (nextCrudItem.clientId == checkedCrudItem?.clientId) { - // This will force a higher log level than exceptions which are caught here. - isolateLogger.warning( - """Potentially previously uploaded CRUD entries are still present in the upload queue. - Make sure to handle uploads and complete CRUD transactions or batches by calling and awaiting their [.complete()] method. - The next upload iteration will be delayed."""); - throw Exception( - 'Delaying due to previously encountered CRUD item.'); - } - - checkedCrudItem = nextCrudItem; - await uploadCrud(); - _updateStatus(uploadError: _noError); - } else { - // Uploading is completed - await adapter.updateLocalTarget(() => getWriteCheckpoint()); - break; - } - } catch (e, stacktrace) { - checkedCrudItem = null; - isolateLogger.warning('Data upload error', e, stacktrace); - _updateStatus(uploading: false, uploadError: e); - await _delayRetry(); - if (!isConnected) { - // Exit the upload loop if the sync stream is no longer connected - break; - } - isolateLogger.warning( - "Caught exception when uploading. Upload will retry after a delay", - e, - stacktrace); - } finally { - _updateStatus(uploading: false); - } - } - }, timeout: retryDelay); - } - - Future getWriteCheckpoint() async { - final credentials = await credentialsCallback(); - if (credentials == null) { - throw CredentialsException("Not logged in"); - } - final uri = - credentials.endpointUri('write-checkpoint2.json?client_id=$clientId'); - - Map headers = { - 'Content-Type': 'application/json', - 'Authorization': "Token ${credentials.token}", - ..._userAgentHeaders - }; - - final response = await _client.get(uri, headers: headers); - if (response.statusCode == 401) { - if (invalidCredentialsCallback != null) { - await invalidCredentialsCallback!(); - } - } - if (response.statusCode != 200) { - throw SyncResponseException.fromResponse(response); - } - - final body = convert.jsonDecode(response.body); - return body['data']['write_checkpoint'] as String; - } - - /// Update sync status based on any non-null parameters. - /// To clear errors, use [_noError] instead of null. - void _updateStatus( - {DateTime? lastSyncedAt, - bool? hasSynced, - bool? connected, - bool? connecting, - bool? downloading, - bool? uploading, - Object? uploadError, - Object? downloadError}) { - final c = connected ?? lastStatus.connected; - var newStatus = SyncStatus( - connected: c, - connecting: !c && (connecting ?? lastStatus.connecting), - lastSyncedAt: lastSyncedAt ?? lastStatus.lastSyncedAt, - hasSynced: hasSynced ?? lastStatus.hasSynced, - downloading: downloading ?? lastStatus.downloading, - uploading: uploading ?? lastStatus.uploading, - uploadError: uploadError == _noError - ? null - : (uploadError ?? lastStatus.uploadError), - downloadError: downloadError == _noError - ? null - : (downloadError ?? lastStatus.downloadError)); - lastStatus = newStatus; - _statusStreamController.add(newStatus); - } - - Future streamingSyncIteration( - {AbortController? abortController}) async { - adapter.startSession(); - final bucketEntries = await adapter.getBucketStates(); - - Map initialBucketStates = {}; - - for (final entry in bucketEntries) { - initialBucketStates[entry.bucket] = entry.opId; - } - - final List buckets = []; - for (var entry in initialBucketStates.entries) { - buckets.add(BucketRequest(entry.key, entry.value)); - } - - Checkpoint? targetCheckpoint; - Checkpoint? validatedCheckpoint; - Checkpoint? appliedCheckpoint; - var bucketSet = Set.from(initialBucketStates.keys); - - var requestStream = streamingSyncRequest( - StreamingSyncRequest(buckets, syncParameters, clientId!)); - - var merged = addBroadcast(requestStream, _localPingController.stream); - - Future? credentialsInvalidation; - bool haveInvalidated = false; - - // Trigger a CRUD upload on reconnect - _internalCrudTriggerController.add(null); - - await for (var line in merged) { - if (aborted) { - break; - } - - _updateStatus(connected: true, connecting: false); - if (line is Checkpoint) { - targetCheckpoint = line; - final Set bucketsToDelete = {...bucketSet}; - final Set newBuckets = {}; - for (final checksum in line.checksums) { - newBuckets.add(checksum.bucket); - bucketsToDelete.remove(checksum.bucket); - } - bucketSet = newBuckets; - await adapter.removeBuckets([...bucketsToDelete]); - _updateStatus(downloading: true); - } else if (line is StreamingSyncCheckpointComplete) { - final result = await adapter.syncLocalDatabase(targetCheckpoint!); - if (!result.checkpointValid) { - // This means checksums failed. Start again with a new checkpoint. - // TODO: better back-off - // await new Promise((resolve) => setTimeout(resolve, 50)); - return false; - } else if (!result.ready) { - // Checksums valid, but need more data for a consistent checkpoint. - // Continue waiting. - } else { - appliedCheckpoint = targetCheckpoint; - - _updateStatus( - downloading: false, - downloadError: _noError, - lastSyncedAt: DateTime.now()); - } - - validatedCheckpoint = targetCheckpoint; - } else if (line is StreamingSyncCheckpointDiff) { - // TODO: It may be faster to just keep track of the diff, instead of the entire checkpoint - if (targetCheckpoint == null) { - throw PowerSyncProtocolException( - 'Checkpoint diff without previous checkpoint'); - } - _updateStatus(downloading: true); - final diff = line; - final Map newBuckets = {}; - for (var checksum in targetCheckpoint.checksums) { - newBuckets[checksum.bucket] = checksum; - } - for (var checksum in diff.updatedBuckets) { - newBuckets[checksum.bucket] = checksum; - } - for (var bucket in diff.removedBuckets) { - newBuckets.remove(bucket); - } - - final newCheckpoint = Checkpoint( - lastOpId: diff.lastOpId, - checksums: [...newBuckets.values], - writeCheckpoint: diff.writeCheckpoint); - targetCheckpoint = newCheckpoint; - - bucketSet = Set.from(newBuckets.keys); - await adapter.removeBuckets(diff.removedBuckets); - adapter.setTargetCheckpoint(targetCheckpoint); - } else if (line is SyncBucketData) { - _updateStatus(downloading: true); - await adapter.saveSyncData(SyncDataBatch([line])); - } else if (line is StreamingSyncKeepalive) { - if (line.tokenExpiresIn == 0) { - // Token expired already - stop the connection immediately - invalidCredentialsCallback?.call().ignore(); - break; - } else if (line.tokenExpiresIn <= 30) { - // Token expires soon - refresh it in the background - if (credentialsInvalidation == null && - invalidCredentialsCallback != null) { - credentialsInvalidation = invalidCredentialsCallback!().then((_) { - // Token has been refreshed - we should restart the connection. - haveInvalidated = true; - // trigger next loop iteration ASAP, don't wait for another - // message from the server. - _localPingController.add(null); - }, onError: (_) { - // Token refresh failed - retry on next keepalive. - credentialsInvalidation = null; - }); - } - } - } else { - if (targetCheckpoint == appliedCheckpoint) { - _updateStatus( - downloading: false, - downloadError: _noError, - lastSyncedAt: DateTime.now()); - } else if (validatedCheckpoint == targetCheckpoint) { - final result = await adapter.syncLocalDatabase(targetCheckpoint!); - if (!result.checkpointValid) { - // This means checksums failed. Start again with a new checkpoint. - // TODO: better back-off - // await new Promise((resolve) => setTimeout(resolve, 50)); - return false; - } else if (!result.ready) { - // Checksums valid, but need more data for a consistent checkpoint. - // Continue waiting. - } else { - appliedCheckpoint = targetCheckpoint; - - _updateStatus( - downloading: false, - downloadError: _noError, - lastSyncedAt: DateTime.now()); - } - } - } - - if (haveInvalidated) { - // Stop this connection, so that a new one will be started - break; - } - } - return true; - } - - Stream streamingSyncRequest(StreamingSyncRequest data) async* { - final credentials = await credentialsCallback(); - if (credentials == null) { - throw CredentialsException('Not logged in'); - } - final uri = credentials.endpointUri('sync/stream'); - - final request = http.Request('POST', uri); - request.headers['Content-Type'] = 'application/json'; - request.headers['Authorization'] = "Token ${credentials.token}"; - request.headers.addAll(_userAgentHeaders); - - request.body = convert.jsonEncode(data); - - http.StreamedResponse res; - try { - // Do not close the client during the request phase - this causes uncaught errors. - _safeToClose = false; - res = await _client.send(request); - } finally { - _safeToClose = true; - } - if (aborted) { - return; - } - - if (res.statusCode == 401) { - if (invalidCredentialsCallback != null) { - await invalidCredentialsCallback!(); - } - } - if (res.statusCode != 200) { - throw await SyncResponseException.fromStreamedResponse(res); - } - - // Note: The response stream is automatically closed when this loop errors - await for (var line in ndjson(res.stream)) { - if (aborted) { - break; - } - yield parseStreamingSyncLine(line as Map); - } - } - - /// Delays the standard `retryDelay` Duration, but exits early if - /// an abort has been requested. - Future _delayRetry() async { - await Future.any([Future.delayed(retryDelay), _abort!.onAbort]); - } -} - -/// Attempt to give a basic summary of the error for cases where the full error -/// is not logged. -String _syncErrorMessage(Object? error) { - if (error == null) { - return 'Unknown'; - } else if (error is http.ClientException) { - return 'Sync service error'; - } else if (error is SyncResponseException) { - if (error.statusCode == 401) { - return 'Authorization error'; - } else { - return 'Sync service error'; - } - } else if (error is ArgumentError || error is FormatException) { - return 'Configuration error'; - } else if (error is CredentialsException) { - return 'Credentials error'; - } else if (error is PowerSyncProtocolException) { - return 'Protocol error'; - } else { - return '${error.runtimeType}'; - } -} diff --git a/packages/powersync/lib/src/sync_status.dart b/packages/powersync/lib/src/sync_status.dart deleted file mode 100644 index f04e7300..00000000 --- a/packages/powersync/lib/src/sync_status.dart +++ /dev/null @@ -1,121 +0,0 @@ -class SyncStatus { - /// true if currently connected. - /// - /// This means the PowerSync connection is ready to download, and - /// [PowerSyncBackendConnector.uploadData] may be called for any local changes. - final bool connected; - - /// true if the PowerSync connection is busy connecting. - /// - /// During this stage, [PowerSyncBackendConnector.uploadData] may already be called, - /// called, and [uploading] may be true. - final bool connecting; - - /// true if actively downloading changes. - /// - /// This is only true when [connected] is also true. - final bool downloading; - - /// true if uploading changes - final bool uploading; - - /// Time that a last sync has fully completed, if any. - /// - /// This is null while loading the database. - final DateTime? lastSyncedAt; - - /// Indicates whether there has been at least one full sync, if any. - /// Is null when unknown, for example when state is still being loaded from the database. - final bool? hasSynced; - - /// Error during uploading. - /// - /// Cleared on the next successful upload. - final Object? uploadError; - - /// Error during downloading (including connecting). - /// - /// Cleared on the next successful data download. - final Object? downloadError; - - const SyncStatus( - {this.connected = false, - this.connecting = false, - this.lastSyncedAt, - this.hasSynced, - this.downloading = false, - this.uploading = false, - this.downloadError, - this.uploadError}); - - @override - bool operator ==(Object other) { - return (other is SyncStatus && - other.connected == connected && - other.downloading == downloading && - other.uploading == uploading && - other.connecting == connecting && - other.downloadError == downloadError && - other.uploadError == uploadError && - other.lastSyncedAt == lastSyncedAt && - other.hasSynced == hasSynced); - } - - SyncStatus copyWith({ - bool? connected, - bool? downloading, - bool? uploading, - bool? connecting, - Object? uploadError, - Object? downloadError, - DateTime? lastSyncedAt, - bool? hasSynced, - }) { - return SyncStatus( - connected: connected ?? this.connected, - downloading: downloading ?? this.downloading, - uploading: uploading ?? this.uploading, - connecting: connecting ?? this.connecting, - uploadError: uploadError ?? this.uploadError, - downloadError: downloadError ?? this.downloadError, - lastSyncedAt: lastSyncedAt ?? this.lastSyncedAt, - hasSynced: hasSynced ?? this.hasSynced, - ); - } - - /// Get the current [downloadError] or [uploadError]. - Object? get anyError { - return downloadError ?? uploadError; - } - - @override - int get hashCode { - return Object.hash(connected, downloading, uploading, connecting, - uploadError, downloadError, lastSyncedAt); - } - - @override - String toString() { - return "SyncStatus"; - } -} - -/// Stats of the local upload queue. -class UploadQueueStats { - /// Number of records in the upload queue. - int count; - - /// Size of the upload queue in bytes. - int? size; - - UploadQueueStats({required this.count, this.size}); - - @override - String toString() { - if (size == null) { - return "UploadQueueStats"; - } else { - return "UploadQueueStats"; - } - } -} diff --git a/packages/powersync/lib/src/sync_types.dart b/packages/powersync/lib/src/sync_types.dart deleted file mode 100644 index 46eeb959..00000000 --- a/packages/powersync/lib/src/sync_types.dart +++ /dev/null @@ -1,146 +0,0 @@ -import 'bucket_storage.dart'; - -class Checkpoint { - final String lastOpId; - final String? writeCheckpoint; - final List checksums; - - const Checkpoint( - {required this.lastOpId, required this.checksums, this.writeCheckpoint}); - - Checkpoint.fromJson(Map json) - : lastOpId = json['last_op_id'], - writeCheckpoint = json['write_checkpoint'], - checksums = (json['buckets'] as List) - .map((b) => BucketChecksum.fromJson(b)) - .toList(); - - Map toJson() { - return { - 'last_op_id': lastOpId, - 'write_checkpoint': writeCheckpoint, - 'buckets': checksums - .map((c) => {'bucket': c.bucket, 'checksum': c.checksum}) - .toList(growable: false) - }; - } -} - -class BucketChecksum { - final String bucket; - final int checksum; - - /// Count is informational only - final int? count; - final String? lastOpId; - - const BucketChecksum( - {required this.bucket, - required this.checksum, - this.count, - this.lastOpId}); - - BucketChecksum.fromJson(Map json) - : bucket = json['bucket'], - checksum = json['checksum'], - count = json['count'], - lastOpId = json['last_op_id']; -} - -class StreamingSyncCheckpoint { - Checkpoint checkpoint; - - StreamingSyncCheckpoint(this.checkpoint); - - StreamingSyncCheckpoint.fromJson(Map json) - : checkpoint = Checkpoint.fromJson(json); -} - -class StreamingSyncCheckpointDiff { - String lastOpId; - List updatedBuckets; - List removedBuckets; - String? writeCheckpoint; - - StreamingSyncCheckpointDiff( - this.lastOpId, this.updatedBuckets, this.removedBuckets); - - StreamingSyncCheckpointDiff.fromJson(Map json) - : lastOpId = json['last_op_id'], - writeCheckpoint = json['write_checkpoint'], - updatedBuckets = (json['updated_buckets'] as List) - .map((e) => BucketChecksum.fromJson(e)) - .toList(), - removedBuckets = List.from(json['removed_buckets']); -} - -class StreamingSyncCheckpointComplete { - String lastOpId; - - StreamingSyncCheckpointComplete(this.lastOpId); - - StreamingSyncCheckpointComplete.fromJson(Map json) - : lastOpId = json['last_op_id']; -} - -class StreamingSyncKeepalive { - int tokenExpiresIn; - - StreamingSyncKeepalive(this.tokenExpiresIn); - - StreamingSyncKeepalive.fromJson(Map json) - : tokenExpiresIn = json['token_expires_in']; -} - -Object? parseStreamingSyncLine(Map line) { - if (line.containsKey('checkpoint')) { - return Checkpoint.fromJson(line['checkpoint']); - } else if (line.containsKey('checkpoint_diff')) { - return StreamingSyncCheckpointDiff.fromJson(line['checkpoint_diff']); - } else if (line.containsKey('checkpoint_complete')) { - return StreamingSyncCheckpointComplete.fromJson( - line['checkpoint_complete']); - } else if (line.containsKey('data')) { - return SyncBucketData.fromJson(line['data']); - } else if (line.containsKey('token_expires_in')) { - return StreamingSyncKeepalive.fromJson(line); - } else { - return null; - } -} - -class StreamingSyncRequest { - List buckets; - bool includeChecksum = true; - String clientId; - Map? parameters; - - StreamingSyncRequest(this.buckets, this.parameters, this.clientId); - - Map toJson() { - final Map json = { - 'buckets': buckets, - 'include_checksum': includeChecksum, - 'raw_data': true, - 'client_id': clientId - }; - - if (parameters != null) { - json['parameters'] = parameters; - } - - return json; - } -} - -class BucketRequest { - String name; - String after; - - BucketRequest(this.name, this.after); - - Map toJson() => { - 'name': name, - 'after': after, - }; -} diff --git a/packages/powersync/lib/src/version.dart b/packages/powersync/lib/src/version.dart deleted file mode 100644 index f3ee1336..00000000 --- a/packages/powersync/lib/src/version.dart +++ /dev/null @@ -1 +0,0 @@ -const String libraryVersion = '1.9.3'; diff --git a/packages/powersync/lib/src/web/powersync_db.worker.dart b/packages/powersync/lib/src/web/powersync_db.worker.dart deleted file mode 100644 index 8ce44f4b..00000000 --- a/packages/powersync/lib/src/web/powersync_db.worker.dart +++ /dev/null @@ -1,33 +0,0 @@ -/// This file needs to be compiled to JavaScript with the command -/// dart compile js -O4 packages/powersync/lib/src/web/powersync_db.worker.dart -o assets/db_worker.js -/// The output should then be included in each project's `web` directory - -library; - -import 'dart:js_interop'; - -import 'package:sqlite_async/sqlite3_web_worker.dart'; -import 'package:sqlite_async/sqlite3_web.dart'; -import 'package:sqlite_async/sqlite3_wasm.dart'; - -import 'worker_utils.dart'; - -void main() { - WebSqlite.workerEntrypoint(controller: PowerSyncAsyncSqliteController()); -} - -final class PowerSyncAsyncSqliteController extends AsyncSqliteController { - @override - Future openDatabase( - WasmSqlite3 sqlite3, String path, String vfs) async { - final asyncDb = await super.openDatabase(sqlite3, path, vfs); - setupPowerSyncDatabase(asyncDb.database); - return asyncDb; - } - - @override - Future handleCustomRequest( - ClientConnection connection, JSAny? request) { - throw UnimplementedError(); - } -} diff --git a/packages/powersync/lib/src/web/worker_utils.dart b/packages/powersync/lib/src/web/worker_utils.dart deleted file mode 100644 index 9f15547f..00000000 --- a/packages/powersync/lib/src/web/worker_utils.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:powersync/sqlite3_common.dart'; -import 'package:powersync/src/open_factory/common_db_functions.dart'; -import 'package:uuid/uuid.dart'; - -// Registers custom SQLite functions for the SQLite connection -void setupPowerSyncDatabase(CommonDatabase database) { - setupCommonDBFunctions(database); - final uuid = Uuid(); - - database.createFunction( - functionName: 'uuid', - argumentCount: const AllowedArgumentCount(0), - function: (args) { - return uuid.v4(); - }, - ); - database.createFunction( - // Postgres compatibility - functionName: 'gen_random_uuid', - argumentCount: const AllowedArgumentCount(0), - function: (args) => uuid.v4(), - ); - database.createFunction( - functionName: 'powersync_sleep', - argumentCount: const AllowedArgumentCount(1), - function: (args) { - // Can't perform synchronous sleep on web - final millis = args[0] as int; - return millis; - }, - ); - - database.createFunction( - functionName: 'powersync_connection_name', - argumentCount: const AllowedArgumentCount(0), - function: (args) { - return 'N/A'; - }, - ); -} diff --git a/packages/powersync/lib/web_worker.dart b/packages/powersync/lib/web_worker.dart index 19983751..5d7b86d3 100644 --- a/packages/powersync/lib/web_worker.dart +++ b/packages/powersync/lib/web_worker.dart @@ -1 +1 @@ -export 'src/web/powersync_db.worker.dart'; +export 'package:powersync_core/src/web/powersync_db.worker.dart'; diff --git a/packages/powersync/pubspec.yaml b/packages/powersync/pubspec.yaml index e7d7b91a..26fffc25 100644 --- a/packages/powersync/pubspec.yaml +++ b/packages/powersync/pubspec.yaml @@ -1,45 +1,25 @@ name: powersync -version: 1.9.3 +version: 1.16.1 homepage: https://powersync.com repository: https://github.com/powersync-ja/powersync.dart -description: PowerSync Flutter SDK - sync engine for building local-first apps. +description: PowerSync Flutter SDK. Sync Postgres, MongoDB or MySQL with SQLite in your Flutter app environment: sdk: ^3.4.0 + dependencies: # Needed because of sqlite3_flutter_libs flutter: sdk: flutter - sqlite_async: ^0.11.0 - # We only use sqlite3 as a transitive dependency, - # but right now we need a minimum of v2.4.6. - sqlite3: ^2.4.6 - universal_io: ^2.0.0 - sqlite3_flutter_libs: ^0.5.23 - powersync_flutter_libs: ^0.4.3 - meta: ^1.0.0 - http: ^1.1.0 - uuid: ^4.2.0 - async: ^2.10.0 - logging: ^1.1.1 + sqlite3_flutter_libs: ^0.5.39 + powersync_core: ^1.6.1 + powersync_flutter_libs: ^0.4.12 collection: ^1.17.0 - fetch_client: ^1.1.2 - pubspec_parse: ^1.3.0 - args: ^2.5.0 - pub_semver: ^2.1.4 - js: ^0.7.0 - web: ^1.0.0 + dev_dependencies: - dcli: ^4.0.0 - lints: ^3.0.0 - test: ^1.25.0 - test_api: ^0.7.0 - path_provider: ^2.0.13 - shelf: ^1.4.1 - shelf_router: ^1.1.4 - shelf_static: ^1.1.2 - stream_channel: ^2.1.2 + lints: ^5.1.0 path: ^1.8.3 + path_provider: ^2.0.13 platforms: android: diff --git a/packages/powersync/test/connected_test.dart b/packages/powersync/test/connected_test.dart deleted file mode 100644 index 0f6d3b71..00000000 --- a/packages/powersync/test/connected_test.dart +++ /dev/null @@ -1,136 +0,0 @@ -@TestOn('!browser') -// This test uses a local server which is possible to control in Web via hybrid main, -// but this makes the test significantly more complex. -import 'dart:async'; - -import 'package:powersync/powersync.dart'; -import 'package:test/test.dart'; - -import 'server/sync_server/mock_sync_server.dart'; -import 'streaming_sync_test.dart'; -import 'utils/abstract_test_utils.dart'; -import 'utils/test_utils_impl.dart'; - -final testUtils = TestUtils(); - -void main() { - group('connected tests', () { - late String path; - setUp(() async { - path = testUtils.dbPath(); - }); - - tearDown(() async { - await testUtils.cleanDb(path: path); - }); - - createTestServer() async { - final testServer = TestHttpServerHelper(); - await testServer.start(); - addTearDown(() => testServer.stop()); - return testServer; - } - - test('should connect to mock PowerSync instance', () async { - final testServer = await createTestServer(); - final connector = TestConnector(() async { - return PowerSyncCredentials( - endpoint: testServer.uri.toString(), - token: 'token not used here', - expiresAt: DateTime.now()); - }); - - final db = PowerSyncDatabase.withFactory( - await testUtils.testFactory(path: path), - schema: defaultSchema, - maxReaders: 3); - await db.initialize(); - - final connectedCompleter = Completer(); - - db.statusStream.listen((status) { - if (status.connected) { - connectedCompleter.complete(); - } - }); - - // Add a basic command for the test server to send - testServer.addEvent('{"token_expires_in": 3600}\n'); - - await db.connect(connector: connector); - await connectedCompleter.future; - - expect(db.connected, isTrue); - await db.disconnect(); - }); - - test('should trigger uploads when connection is re-established', () async { - int uploadCounter = 0; - Completer uploadTriggeredCompleter = Completer(); - final testServer = await createTestServer(); - final connector = TestConnector(() async { - return PowerSyncCredentials( - endpoint: testServer.uri.toString(), - token: 'token not used here', - expiresAt: DateTime.now()); - }, uploadData: (database) async { - uploadCounter++; - uploadTriggeredCompleter.complete(); - throw Exception('No uploads occur here'); - }); - - final db = PowerSyncDatabase.withFactory( - await testUtils.testFactory(path: path), - schema: defaultSchema, - maxReaders: 3); - await db.initialize(); - - // Create an item which should trigger an upload. - await db.execute( - 'INSERT INTO customers (id, name) VALUES (uuid(), ?)', ['steven']); - - // Create a new completer to await the next upload - uploadTriggeredCompleter = Completer(); - - // Connect the PowerSync instance - final connectedCompleter = Completer(); - // The first connection attempt will fail - final connectedErroredCompleter = Completer(); - - db.statusStream.listen((status) { - if (status.connected && !connectedCompleter.isCompleted) { - connectedCompleter.complete(); - } - if (status.downloadError != null && - !connectedErroredCompleter.isCompleted) { - connectedErroredCompleter.complete(); - } - }); - - // The first command will not be valid, this simulates a failed connection - testServer.addEvent('asdf\n'); - await db.connect(connector: connector); - - // The connect operation should have triggered an upload (even though it fails to connect) - await uploadTriggeredCompleter.future; - expect(uploadCounter, equals(1)); - // Create a new completer for the next iteration - uploadTriggeredCompleter = Completer(); - - // Connection attempt should initially fail - await connectedErroredCompleter.future; - expect(db.currentStatus.anyError, isNotNull); - - // Now send a valid command. Which will result in successful connection - await testServer.clearEvents(); - testServer.addEvent('{"token_expires_in": 3600}\n'); - await connectedCompleter.future; - expect(db.connected, isTrue); - - await uploadTriggeredCompleter.future; - expect(uploadCounter, equals(2)); - - await db.disconnect(); - }); - }); -} diff --git a/packages/powersync/test/server/sync_server/mock_sync_server.dart b/packages/powersync/test/server/sync_server/mock_sync_server.dart deleted file mode 100644 index 0f1c8f49..00000000 --- a/packages/powersync/test/server/sync_server/mock_sync_server.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:shelf/shelf.dart'; -import 'package:shelf/shelf_io.dart' as io; -import 'package:shelf_router/shelf_router.dart'; - -// A basic Mock PowerSync service server which queues commands -// which clients can receive via connecting to the `/sync/stream` route. -// This assumes only one client will ever be connected at a time. -class TestHttpServerHelper { - // Use a queued stream to make tests easier. - StreamController _controller = StreamController(); - late HttpServer _server; - Uri get uri => Uri.parse('http://localhost:${_server.port}'); - - Future start() async { - final router = Router() - ..post('/sync/stream', (Request request) async { - // Respond immediately with a stream - return Response.ok(_controller.stream.transform(utf8.encoder), - headers: { - 'Content-Type': 'application/x-ndjson', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - }, - context: { - "shelf.io.buffer_output": false - }); - }); - - _server = await io.serve(router.call, 'localhost', 0); - print('Test server running at ${_server.address}:${_server.port}'); - } - - // Queue events which will be sent to connected clients. - void addEvent(String data) { - _controller.add(data); - } - - // Clear events. We rely on a buffered controller here. Create a new controller - // in order to clear the buffer. - Future clearEvents() async { - await _controller.close(); - _controller = StreamController(); - } - - Future stop() async { - await _controller.close(); - await _server.close(); - } -} diff --git a/packages/powersync/test/streaming_sync_test.dart b/packages/powersync/test/streaming_sync_test.dart deleted file mode 100644 index 92b03dd3..00000000 --- a/packages/powersync/test/streaming_sync_test.dart +++ /dev/null @@ -1,161 +0,0 @@ -@TestOn('!browser') -// TODO setup hybrid server -import 'dart:async'; -import 'dart:math'; - -import 'package:powersync/powersync.dart'; -import 'package:test/test.dart'; - -import 'test_server.dart'; -import 'utils/test_utils_impl.dart'; - -final testUtils = TestUtils(); - -class TestConnector extends PowerSyncBackendConnector { - final Function _fetchCredentials; - final Future Function(PowerSyncDatabase)? _uploadData; - - TestConnector(this._fetchCredentials, - {Future Function(PowerSyncDatabase)? uploadData}) - : _uploadData = uploadData; - - @override - Future fetchCredentials() { - return _fetchCredentials(); - } - - @override - Future uploadData(PowerSyncDatabase database) async { - await _uploadData?.call(database); - } -} - -void main() { - group('Streaming Sync Test', () { - late String path; - - setUp(() async { - path = testUtils.dbPath(); - await testUtils.cleanDb(path: path); - }); - - tearDown(() async { - await testUtils.cleanDb(path: path); - }); - - test('full powersync reconnect', () async { - // Test repeatedly creating new PowerSync connections, then disconnect - // and close the connection. - final random = Random(); - - for (var i = 0; i < 10; i++) { - var server = await createServer(); - - credentialsCallback() async { - return PowerSyncCredentials( - endpoint: server.endpoint, token: 'token'); - } - - final pdb = await testUtils.setupPowerSync(path: path); - pdb.retryDelay = Duration(milliseconds: 5000); - var connector = TestConnector(credentialsCallback); - pdb.connect(connector: connector); - - await Future.delayed(Duration(milliseconds: random.nextInt(100))); - if (random.nextBool()) { - server.close(); - } - - await pdb.close(); - - // Give some time for connections to close - final watch = Stopwatch()..start(); - while (server.connectionCount != 0 && watch.elapsedMilliseconds < 100) { - await Future.delayed(Duration(milliseconds: random.nextInt(10))); - } - - expect(server.connectionCount, equals(0)); - expect(server.maxConnectionCount, lessThanOrEqualTo(1)); - - server.close(); - } - }); - - test('powersync connection errors', () async { - // Test repeatedly killing the streaming connection - // Errors like this are expected: - // - // [PowerSync] WARNING: 2023-06-29 16:05:24.810002: Sync error - // Connection closed while receiving data - // Write failed - // Connection refused - // - // Errors like this are not okay: - // [PowerSync] WARNING: 2023-06-29 16:10:17.667537: Sync Isolate error - // [Connection closed while receiving data, #0 IOClient.send. (package:http/src/io_client.dart:76:13) - - TestServer? server; - - credentialsCallback() async { - if (server == null) { - throw AssertionError('No active server'); - } - return PowerSyncCredentials(endpoint: server.endpoint, token: 'token'); - } - - final pdb = await testUtils.setupPowerSync(path: path); - pdb.retryDelay = const Duration(milliseconds: 5); - var connector = TestConnector(credentialsCallback); - pdb.connect(connector: connector); - - for (var i = 0; i < 10; i++) { - server = await createServer(); - - // var stream = impl.streamingSyncRequest(StreamingSyncRequest([])); - // 2ms: HttpException: HttpServer is not bound to a socket - // 20ms: Connection closed while receiving data - await Future.delayed(Duration(milliseconds: 20)); - server.close(); - } - await pdb.close(); - }); - - test('multiple connect calls', () async { - // Test calling connect() multiple times. - // We check that this does not cause multiple connections to be opened concurrently. - final random = Random(); - var server = await createServer(); - - credentialsCallback() async { - return PowerSyncCredentials(endpoint: server.endpoint, token: 'token'); - } - - final pdb = await testUtils.setupPowerSync(path: path); - pdb.retryDelay = Duration(milliseconds: 5000); - var connector = TestConnector(credentialsCallback); - pdb.connect(connector: connector); - pdb.connect(connector: connector); - - final watch = Stopwatch()..start(); - - // Wait for at least one connection - while (server.connectionCount < 1 && watch.elapsedMilliseconds < 500) { - await Future.delayed(Duration(milliseconds: random.nextInt(10))); - } - // Give some time for a second connection if any - await Future.delayed(Duration(milliseconds: random.nextInt(50))); - - await pdb.close(); - - // Give some time for connections to close - while (server.connectionCount != 0 && watch.elapsedMilliseconds < 1000) { - await Future.delayed(Duration(milliseconds: random.nextInt(10))); - } - - expect(server.connectionCount, equals(0)); - expect(server.maxConnectionCount, equals(1)); - - server.close(); - }); - }); -} diff --git a/packages/powersync/test/utils/abstract_test_utils.dart b/packages/powersync/test/utils/abstract_test_utils.dart deleted file mode 100644 index 773dafb6..00000000 --- a/packages/powersync/test/utils/abstract_test_utils.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:logging/logging.dart'; -import 'package:powersync/powersync.dart'; -import 'package:sqlite_async/sqlite3_common.dart'; -import 'package:sqlite_async/sqlite_async.dart'; -import 'package:test_api/src/backend/invoker.dart'; - -const schema = Schema([ - Table('assets', [ - Column.text('created_at'), - Column.text('make'), - Column.text('model'), - Column.text('serial_number'), - Column.integer('quantity'), - Column.text('user_id'), - Column.text('customer_id'), - Column.text('description'), - ], indexes: [ - Index('makemodel', [IndexedColumn('make'), IndexedColumn('model')]) - ]), - Table('customers', [Column.text('name'), Column.text('email')]) -]); - -const defaultSchema = schema; - -final testLogger = _makeTestLogger(); - -final testWarningLogger = _makeTestLogger(level: Level.WARNING); - -Logger _makeTestLogger({Level level = Level.ALL, String? name}) { - final logger = Logger.detached(name ?? 'PowerSync Tests'); - logger.level = level; - logger.onRecord.listen((record) { - print( - '[${record.loggerName}] ${record.level.name}: ${record.time}: ${record.message}'); - if (record.error != null) { - print(record.error); - } - if (record.stackTrace != null) { - print(record.stackTrace); - } - - if (record.error != null && record.level >= Level.SEVERE) { - // Hack to fail the test if a SEVERE error is logged. - // Not ideal, but works to catch "Sync Isolate error". - uncaughtError() async { - throw record.error!; - } - - uncaughtError(); - } - }); - return logger; -} - -abstract class AbstractTestUtils { - String get _testName => Invoker.current!.liveTest.test.name; - - String dbPath() { - var testShortName = - _testName.replaceAll(RegExp(r'[\s\./]'), '_').toLowerCase(); - var dbName = "test-db/$testShortName.db"; - return dbName; - } - - /// Generates a test open factory - Future testFactory( - {String? path, - String sqlitePath = '', - SqliteOptions options = const SqliteOptions.defaults()}) async { - return PowerSyncOpenFactory(path: path ?? dbPath(), sqliteOptions: options); - } - - /// Creates a SqliteDatabaseConnection - Future setupPowerSync( - {String? path, Schema? schema, Logger? logger}) async { - final db = PowerSyncDatabase.withFactory(await testFactory(path: path), - schema: schema ?? defaultSchema, - logger: logger ?? _makeTestLogger(name: _testName)); - await db.initialize(); - return db; - } - - Future setupSqlite( - {required PowerSyncDatabase powersync}) async { - await powersync.initialize(); - - final sqliteDb = - await powersync.isolateConnectionFactory().openRawDatabase(); - - return sqliteDb; - } - - /// Deletes any DB data - Future cleanDb({required String path}); -} diff --git a/packages/powersync/test/utils/stub_test_utils.dart b/packages/powersync/test/utils/stub_test_utils.dart deleted file mode 100644 index 3f86512c..00000000 --- a/packages/powersync/test/utils/stub_test_utils.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'abstract_test_utils.dart'; - -class TestUtils extends AbstractTestUtils { - @override - Future cleanDb({required String path}) { - throw UnimplementedError(); - } -} diff --git a/packages/powersync_attachments_helper/CHANGELOG.md b/packages/powersync_attachments_helper/CHANGELOG.md index 74acae9b..f80b8798 100644 --- a/packages/powersync_attachments_helper/CHANGELOG.md +++ b/packages/powersync_attachments_helper/CHANGELOG.md @@ -1,3 +1,67 @@ +## 0.6.20 + + - Add note about new attachment queue system in core package. + +## 0.6.19 + + - Remove direct dependency on `sqlite_async`. + +## 0.6.18+11 + + - Update a dependency to the latest release. + +## 0.6.18+10 + + - Update a dependency to the latest release. + +## 0.6.18+9 + + - Update a dependency to the latest release. + +## 0.6.18+8 + + - Update a dependency to the latest release. + +## 0.6.18+7 + + - Update a dependency to the latest release. + +## 0.6.18+6 + + - Update a dependency to the latest release. + +## 0.6.18+5 + + - Update a dependency to the latest release. + +## 0.6.18+4 + + - Update a dependency to the latest release. + +## 0.6.18+3 + + - Update a dependency to the latest release. + +## 0.6.18+2 + + - Update a dependency to the latest release. + +## 0.6.18+1 + + - Update a dependency to the latest release. + +## 0.6.18 + + - Update a dependency to the latest release. + +## 0.6.17 + + - **FEAT**: PowerSync encryption with SQLCipher ([#194](https://github.com/powersync-ja/powersync.dart/issues/194)). ([ea6186d7](https://github.com/powersync-ja/powersync.dart/commit/ea6186d7d844d976fafb0c2e2e8a9f25e4deb08c)) + +## 0.6.16 + + - Update a dependency to the latest release. + ## 0.6.15+2 - Update a dependency to the latest release. diff --git a/packages/powersync_attachments_helper/README.md b/packages/powersync_attachments_helper/README.md index a44f8c9d..c9b5f532 100644 --- a/packages/powersync_attachments_helper/README.md +++ b/packages/powersync_attachments_helper/README.md @@ -1,17 +1,31 @@ # PowerSync Attachments Helper for Dart/Flutter -[PowerSync Attachments Helper](https://pub.dev/packages/powersync_attachments_helper) is a package that assist in keeping files in sync with local and remote storage. +[PowerSync Attachments Helper](https://pub.dev/packages/powersync_attachments_helper) is a package that assists in keeping files in sync between local and remote storage. + +> [!WARNING] +> This package will eventually be replaced by a new attachments helper library in the core PowerSync package, available through: +> ```dart +> package:powersync_core/attachments/attachments.dart +> ``` +> +> The `powersync_core/attachments` library is in alpha and brings improved APIs and functionality that is more in line with our other SDKs, such as the ability to write your own local storage implementation. +> +> Check out the [docs here](https://pub.dev/documentation/powersync_core/latest/topics/attachments-topic.html) to get started. +> +> While the `powersync_attachments_helper` package will still get bug fixes if you need them, +> new features will only be developed on `powersync_core/attachments`. + ## Features -* Handles syncing uploads, downloads and deletes between local and remote storage. +- Handles syncing uploads, downloads and deletes between local and remote storage. ## Getting started ```dart import 'dart:async'; import 'package:powersync_attachments_helper/powersync_attachments_helper.dart'; -import 'package:powersync/powersync.dart'; +import 'package:powersync_core/powersync_core.dart'; // Set up schema with an id field that can be used in watchIds(). // In this case it is photo_id @@ -83,5 +97,3 @@ initializeAttachmentQueue(PowerSyncDatabase db) async { await attachmentQueue.init(); } ``` - -See our [Supabase Flutter To-Do List example app](../../demos/supabase-todolist/README.md) for a concrete implementation of the above. diff --git a/packages/powersync_attachments_helper/analysis_options.yaml b/packages/powersync_attachments_helper/analysis_options.yaml index dee8927a..915b40b5 100644 --- a/packages/powersync_attachments_helper/analysis_options.yaml +++ b/packages/powersync_attachments_helper/analysis_options.yaml @@ -11,7 +11,13 @@ # (the recommended set includes the core lints). # The core lints are also what is used by pub.dev for scoring packages. -include: package:lints/recommended.yaml +include: package:flutter_lints/flutter.yaml + +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true # Uncomment the following section to specify additional rules. diff --git a/packages/powersync_attachments_helper/example/getting_started.dart b/packages/powersync_attachments_helper/example/getting_started.dart index f5302508..037046f0 100644 --- a/packages/powersync_attachments_helper/example/getting_started.dart +++ b/packages/powersync_attachments_helper/example/getting_started.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:powersync_attachments_helper/powersync_attachments_helper.dart'; -import 'package:powersync/powersync.dart'; +import 'package:powersync_core/powersync_core.dart'; const schema = Schema([ Table('users', [Column.text('name'), Column.text('photo_id')]) @@ -14,12 +14,13 @@ late AbstractRemoteStorageAdapter remoteStorage; late PhotoAttachmentQueue attachmentQueue; class PhotoAttachmentQueue extends AbstractAttachmentQueue { - PhotoAttachmentQueue(db, remoteStorage) + PhotoAttachmentQueue( + PowerSyncDatabase db, AbstractRemoteStorageAdapter remoteStorage) : super(db: db, remoteStorage: remoteStorage); @override Future saveFile(String fileId, int size, - {mediaType = 'image/jpeg'}) async { + {String mediaType = 'image/jpeg'}) async { String filename = '$fileId.jpg'; Attachment photoAttachment = Attachment( id: fileId, @@ -60,7 +61,7 @@ class PhotoAttachmentQueue extends AbstractAttachmentQueue { } } -initializeAttachmentQueue(PowerSyncDatabase db) async { +Future initializeAttachmentQueue(PowerSyncDatabase db) async { attachmentQueue = PhotoAttachmentQueue(db, remoteStorage); await attachmentQueue.init(); } diff --git a/packages/powersync_attachments_helper/lib/src/attachments_queue.dart b/packages/powersync_attachments_helper/lib/src/attachments_queue.dart index 7ceb8208..08dd48ce 100644 --- a/packages/powersync_attachments_helper/lib/src/attachments_queue.dart +++ b/packages/powersync_attachments_helper/lib/src/attachments_queue.dart @@ -6,7 +6,7 @@ import './local_storage_adapter.dart'; import './remote_storage_adapter.dart'; import './syncing_service.dart'; import 'package:logging/logging.dart'; -import 'package:powersync/powersync.dart'; +import 'package:powersync_core/powersync_core.dart'; /// Logger for the attachment queue final log = Logger('AttachmentQueue'); @@ -71,7 +71,7 @@ abstract class AbstractAttachmentQueue { /// 1. Creating attachments directory /// 2. Adding watches for uploads, downloads, and deletes /// 3. Adding trigger to run uploads, downloads, and deletes when device is online after being offline - init() async { + Future init() async { // Ensure the directory where attachments are downloaded, exists await localStorage.makeDir(await getStorageDirectory()); @@ -93,7 +93,7 @@ abstract class AbstractAttachmentQueue { }); } - _trigger() async { + Future _trigger() async { await syncingService.runSync(); } diff --git a/packages/powersync_attachments_helper/lib/src/attachments_queue_table.dart b/packages/powersync_attachments_helper/lib/src/attachments_queue_table.dart index 91e940b5..938ad4b1 100644 --- a/packages/powersync_attachments_helper/lib/src/attachments_queue_table.dart +++ b/packages/powersync_attachments_helper/lib/src/attachments_queue_table.dart @@ -1,5 +1,5 @@ -import 'package:powersync/powersync.dart'; -import 'package:powersync/sqlite3_common.dart' as sqlite; +import 'package:powersync_core/powersync_core.dart'; +import 'package:powersync_core/sqlite3_common.dart' as sqlite; const defaultAttachmentsQueueTableName = 'attachments_queue'; @@ -50,13 +50,14 @@ class Attachment { factory Attachment.fromRow(sqlite.Row row) { return Attachment( - id: row['id'], - filename: row['filename'], - localUri: row['local_uri'], - size: row['size'], - mediaType: row['media_type'], - timestamp: row['timestamp'], - state: row['state']); + id: row['id'] as String, + filename: row['filename'] as String, + localUri: row['local_uri'] as String?, + size: row['size'] as int?, + mediaType: row['media_type'] as String?, + timestamp: row['timestamp'] as int?, + state: row['state'] as int, + ); } } diff --git a/packages/powersync_attachments_helper/lib/src/attachments_service.dart b/packages/powersync_attachments_helper/lib/src/attachments_service.dart index 0383a393..e464e626 100644 --- a/packages/powersync_attachments_helper/lib/src/attachments_service.dart +++ b/packages/powersync_attachments_helper/lib/src/attachments_service.dart @@ -1,8 +1,8 @@ import './attachments_queue.dart'; import './attachments_queue_table.dart'; import './local_storage_adapter.dart'; -import 'package:powersync/powersync.dart'; -import 'package:powersync/sqlite3_common.dart'; +import 'package:powersync_core/powersync_core.dart'; +import 'package:powersync_core/sqlite3_common.dart'; /// Service for interacting with the attachment queue. class AttachmentsService { @@ -15,7 +15,7 @@ class AttachmentsService { this.attachmentsQueueTableName); /// Table used for storing attachments in the attachment queue. - get table { + String get table { return attachmentsQueueTableName; } diff --git a/packages/powersync_attachments_helper/lib/src/syncing_service.dart b/packages/powersync_attachments_helper/lib/src/syncing_service.dart index 93c2300c..04897329 100644 --- a/packages/powersync_attachments_helper/lib/src/syncing_service.dart +++ b/packages/powersync_attachments_helper/lib/src/syncing_service.dart @@ -4,7 +4,7 @@ import 'dart:typed_data'; import './attachments_queue.dart'; import './local_storage_adapter.dart'; import './remote_storage_adapter.dart'; -import 'package:powersync/powersync.dart'; +import 'package:powersync_core/powersync_core.dart'; import 'attachments_queue_table.dart'; import 'attachments_service.dart'; @@ -14,7 +14,7 @@ class SyncingService { final AbstractRemoteStorageAdapter remoteStorage; final LocalStorageAdapter localStorage; final AttachmentsService attachmentsService; - final Function getLocalUri; + final Future Function(String name) getLocalUri; final Future Function(Attachment attachment, Object exception)? onDownloadError; final Future Function(Attachment attachment, Object exception)? @@ -165,7 +165,7 @@ class SyncingService { } /// Process ID's to be included in the attachment queue. - processIds(List ids, String fileExtension) async { + Future processIds(List ids, String fileExtension) async { List attachments = List.empty(growable: true); for (String id in ids) { @@ -188,7 +188,7 @@ class SyncingService { } /// Delete attachments which have been archived - deleteArchivedAttachments() async { + Future deleteArchivedAttachments() async { await db.execute(''' DELETE FROM ${attachmentsService.table} WHERE state = ${AttachmentState.archived.index} diff --git a/packages/powersync_attachments_helper/pubspec.yaml b/packages/powersync_attachments_helper/pubspec.yaml index 53779451..971228f9 100644 --- a/packages/powersync_attachments_helper/pubspec.yaml +++ b/packages/powersync_attachments_helper/pubspec.yaml @@ -1,6 +1,6 @@ name: powersync_attachments_helper description: A helper library for handling attachments when using PowerSync. -version: 0.6.15+2 +version: 0.6.20 repository: https://github.com/powersync-ja/powersync.dart homepage: https://www.powersync.com/ environment: @@ -10,13 +10,12 @@ dependencies: flutter: sdk: flutter - powersync: ^1.9.3 + powersync_core: ^1.6.1 logging: ^1.2.0 - sqlite_async: ^0.11.0 path_provider: ^2.0.13 dev_dependencies: - lints: ^3.0.0 + flutter_lints: ^5.0.0 test: ^1.25.2 platforms: diff --git a/packages/powersync_core/.gitignore b/packages/powersync_core/.gitignore new file mode 100644 index 00000000..ddfb9c2a --- /dev/null +++ b/packages/powersync_core/.gitignore @@ -0,0 +1,9 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock + +/test-db \ No newline at end of file diff --git a/packages/powersync_core/CHANGELOG.md b/packages/powersync_core/CHANGELOG.md new file mode 100644 index 00000000..4ba23772 --- /dev/null +++ b/packages/powersync_core/CHANGELOG.md @@ -0,0 +1,119 @@ +## 1.6.1 + + - Web: Fix decoding sync streams on status. + + - **DOCS**: Point to uses in example. ([4f4da24e](https://github.com/powersync-ja/powersync.dart/commit/4f4da24e580dec6b1d29a5e0907b83ba7c55e3d8)) + +## 1.6.0 + +- Add `getCrudTransactions()` returning a stream of completed transactions for uploads. +- Add experimental support for [sync streams](https://docs.powersync.com/usage/sync-streams). +- Add new attachments helper implementation in `package:powersync_core/attachments/attachments.dart`. +- Add SwiftPM support. +- Add support for compiling `powersync_core` with `build_web_compilers`. + +## 1.5.2 + + - Fix excessive memory consumption during large sync. + +## 1.5.1 + + - Support latest versions of `package:sqlite3` and `package:sqlite_async`. + - Stream client: Improve `disconnect()` while a connection is being opened. + - Stream client: Support binary sync lines with Rust client and compatible PowerSync service versions. + - Sync client: Improve parsing error responses. + +## 1.5.0 + + - Update the PowerSync core extension to `0.4.2`. + - Add support for [raw tables](https://docs.powersync.com/usage/use-case-examples/raw-tables), which are user-managed + regular SQLite tables instead of the JSON-based views managed by PowerSync. + +## 1.4.1 + + - Rust client: Fix uploading local writes after reconnect. + - `PowerSyncDatabase.withDatabase`: Rename `loggers` parameter to `logger` for consistency. + - Fix parsing HTTP errors for sync service unavailability. + +## 1.4.0 + +Add a new sync client implementation written in Rust instead of Dart. While +this client is still experimental, we intend to make it the default in the +future. The main benefit of this client is faster sync performance, but +upcoming features will also require this client. +We encourage interested users to try it out by passing `SyncOptions` to the +`connect` method: + +```dart +database.connect( + connector: YourConnector(), + options: const SyncOptions( + syncImplementation: SyncClientImplementation.rust, + ), +); +``` + +Switching between the clients can be done at any time without compatibility +issues. If you run into issues with the new client, please reach out to us! + +## 1.3.1 + +- Use `package:http` instead of `package:fetch_client` on the web (since the former now uses fetch as well). +- Allow disconnecting in the credentials callback of a connector. +- Deprecate retry and CRUD upload durations as fields and independent parameters. Use the new `SyncOptions` class instead. +- Fix sync progress report after a compaction or defragmentation on the sync service. + +## 1.3.0 + +* Report real-time progress information about downloads through `SyncStatus.downloadProgress`. +* Add `trackPreviousValues` option on `Table` which sets `CrudEntry.previousValues` to previous values on updates. +* Add `trackMetadata` option on `Table` which adds a `_metadata` column that can be used for updates. + The configured metadata is available through `CrudEntry.metadata`. +* Add `ignoreEmptyUpdates` option which skips creating CRUD entries for updates that don't change any values. + +## 1.2.4 + + - Fix deadlock when `connect()` is called immediately after opening a database. + +## 1.2.3 + + - Introduce locks to avoid duplicate sync streams when multiple instances of the same database are opened. + - Refactor connect / disconnect internally. + - Warn when multiple instances of the same database are opened. + - Fix race condition causing data not to be applied while an upload is in progress. + - Web: Fix token invalidation logic when a sync worker is used. + +## 1.2.2 + + - Fix handling token invalidation on the web. + +## 1.2.1 + + - Raise minimum version of core extension to 0.3.11. + +## 1.2.0 + + - Support bucket priorities and partial syncs. + +## 1.1.3 + + - Add explicit casts in sync service, avoiding possible issues with dart2js optimizations. + +## 1.1.2 + + - Web: Support running in contexts where web workers are unavailable. + - Web: Fix sync worker logs not being disabled. + - `powersync_sqlcipher`: Web support. + +## 1.1.1 + +- Fix `statusStream` emitting the same sync status multiple times. + +## 1.1.0 + + - Increase limit on number of columns per table to 1999. + - Avoid deleting the $local bucket on connect(). + +## 1.0.0 + + - Dart library for Powersync for use cases such as server-side Dart or non-Flutter Dart environments initial release. diff --git a/packages/powersync_core/LICENSE b/packages/powersync_core/LICENSE new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/packages/powersync_core/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/packages/powersync_core/NOTICE b/packages/powersync_core/NOTICE new file mode 100644 index 00000000..da0d4ce5 --- /dev/null +++ b/packages/powersync_core/NOTICE @@ -0,0 +1 @@ +Copyright 2024 Journey Mobile, Inc. diff --git a/packages/powersync_core/README.md b/packages/powersync_core/README.md new file mode 100644 index 00000000..642baa15 --- /dev/null +++ b/packages/powersync_core/README.md @@ -0,0 +1,36 @@ +

+ +

+ +# PowerSync SDK for Dart + +*[PowerSync](https://www.powersync.com) is a sync engine for building local-first apps with instantly-responsive UI/UX and simplified state transfer. Syncs between SQLite on the client-side and Postgres, MongoDB or MySQL on the server-side.* + +This package (`powersync_core`) is the PowerSync client SDK for Dart. + +> **Note** +> +> This is a Dart library for Powersync for use cases such as server-side Dart or non-Flutter Dart environments. +> This library requires sqlite3 with extension loading enabled to be installed on your system. +> +> If you are developing a Flutter application, use [powersync](https://pub.dev/packages/powersync) or [powersync_sqlcipher](https://pub.dev/packages/powersync_sqlcipher) instead. The `powersync_core` package is for non-Flutter Dart environments. + +# Installation + +```bash +dart pub add powersync_core +``` + +# Changelog + +A changelog for this SDK is available [here](https://pub.dev/packages/powersync_core/changelog). + +# API Reference + +The full API reference for this SDK can be found [here](https://pub.dev/documentation/powersync_core/latest/powersync_core/powersync_core-library.html). + +# Found a bug or need help? + +- Join our [Discord server](https://discord.gg/powersync) where you can browse topics from our community, ask questions, share feedback, or just say hello :) +- Please open a [GitHub issue](https://github.com/powersync-ja/powersync.dart/issues) when you come across a bug. +- Have feedback or an idea? [Submit an idea](https://roadmap.powersync.com/tabs/5-roadmap/submit-idea) via our public roadmap or [schedule a chat](https://calendly.com/powersync/powersync-chat) with someone from our product team. diff --git a/packages/powersync_core/analysis_options.yaml b/packages/powersync_core/analysis_options.yaml new file mode 100644 index 00000000..4be9ff7b --- /dev/null +++ b/packages/powersync_core/analysis_options.yaml @@ -0,0 +1,36 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/packages/powersync_core/assets/.gitkeep b/packages/powersync_core/assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/powersync_core/dartdoc_options.yaml b/packages/powersync_core/dartdoc_options.yaml new file mode 100644 index 00000000..4fe2c4e0 --- /dev/null +++ b/packages/powersync_core/dartdoc_options.yaml @@ -0,0 +1,5 @@ +dartdoc: + categories: + attachments: + name: Attachments + markdown: doc/attachments.md diff --git a/packages/powersync_core/doc/attachments.md b/packages/powersync_core/doc/attachments.md new file mode 100644 index 00000000..6919430f --- /dev/null +++ b/packages/powersync_core/doc/attachments.md @@ -0,0 +1,135 @@ +## Attachments + +In many cases, you might want to sync large binary data (like images) along with the data synced by +PowerSync. +Embedding this data directly in your source databases is [inefficient and not recommended](https://docs.powersync.com/usage/use-case-examples/attachments). + +Instead, the PowerSync SDK for Dart and Flutter provides utilities you can use to _reference_ this binary data +in your regular database schema, and then download it from a secondary data store such as Supabase Storage or S3. +Because binary data is not directly stored in the source database in this model, we call these files _attachments_. + + +> [!NOTE] +> These attachment utilities are recommended over our legacy [PowerSync Attachments Helper](https://pub.dev/packages/powersync_attachments_helper) package. The new utilities provide cleaner APIs that are more aligned with similar helpers in our other SDKs, and include improved features such as: +> - Support for writing your own local storage implementation +> - Support for dynamic nested directories and custom per-attachment file extensions out of the box +> - Ability to add optional `metaData` to attachments +> - No longer depends on `dart:io` +> +> If you are new to handling attachments, we recommend starting with these utilities. If you currently use the legacy `powersync_attachments_helper` package, a fairly simple migration would be to adopt the new utilities with a different table name and drop the legacy package. This means existing attachments are lost, but they should be downloaded again. + +## Alpha release + +The attachment utilities described in this document are currently in an alpha state, intended for testing. +Expect breaking changes and instability as development continues. +The attachments API is marked as `@experimental` for this reason. + +Do not rely on these utilities for production use. + +## Usage + +An `AttachmentQueue` instance is used to manage and sync attachments in your app. +The attachments' state is stored in a local-only attachments table. + +### Key assumptions + +- Each attachment is identified by a unique id. +- Attachments are immutable once created. +- Relational data should reference attachments using a foreign key column. +- Relational data should reflect the holistic state of attachments at any given time. Any existing local attachment + will be deleted locally if no relational data references it. + +### Example implementation + +See the [supabase-todolist](https://github.com/powersync-ja/powersync.dart/tree/main/demos/supabase-todolist) demo for a basic example of attachment syncing. + +In particular, relevant snippets from that example are: + +- The attachment queue is set up [here](https://github.com/powersync-ja/powersync.dart/blob/98d73e2f157a697786373fef755576505abc74a5/demos/supabase-todolist/lib/attachments/queue.dart#L16-L36), using conditional imports to store attachments in the file system on native platforms and in-memory for web. +- When a new attachment is added, `saveFile` is called [here](https://github.com/powersync-ja/powersync.dart/blob/98d73e2f157a697786373fef755576505abc74a5/demos/supabase-todolist/lib/attachments/queue.dart#L38-L55) and automatically updates references in the main schema to reference the attachment. + +### Setup + +First, add a table storing local attachment state to your database schema. + +```dart +final schema = Schema([ + AttachmentsQueueTable(), + // In this document, we assume the photo_id column of the todos table references an optional photo + // stored as an attachment. + Table('todos', [ + Column.text('list_id'), + Column.text('photo_id'), + Column.text('description'), + Column.integer('completed'), + ]), +]); +``` + +Next, create an `AttachmentQueue` instance. This class provides default syncing utilities and implements a default +sync strategy. This class can be extended for custom functionality, if needed. + +```dart +final directory = await getApplicationDocumentsDirectory(); + +final attachmentQueue = AttachmentQueue( + db: db, + remoteStorage: SupabaseStorageAdapter(), // instance responsible for uploads and downloads + logger: logger, + localStorage: IOLocalStorage(appDocDir), // IOLocalStorage requires `dart:io` and is not available on the web + watchAttachments: () => db.watch(''' + SELECT photo_id as id FROM todos WHERE photo_id IS NOT NULL + ''').map((results) => [ + for (final row in results) + WatchedAttachmentItem( + id: row['id'] as String, + fileExtension: 'jpg', + ) + ], + ), +); +``` + +Here, + - An instance of `LocalStorageAdapter`, such as the `IOLocalStorage` provided by the SDK, is responsible for storing + attachment contents locally. + - An instance of `RemoteStorageAdapter` is responsible for downloading and uploading attachment contents to the secondary + service, such as S3, Firebase cloud storage or Supabase storage. + - `watchAttachments` is a function emitting a stream of attachment items that are considered to be referenced from + the current database state. In this example, `todos.photo_id` is the only column referencing attachments. + +Next, start the sync process by calling `attachmentQueue.startSync()`. + +## Storing attachments + +To create a new attachment locally, call `AttachmentQueue.saveFile`. To represent the attachment, this method takes +the contents to store, the media type, an optional file extension and id. +The queue will store the contents in a local file and mark it as queued for upload. It also invokes a callback +responsible for referencing the id of the generated attachment in the primary data model: + +```dart +Future savePhotoAttachment( + Stream> photoData, String todoId, + {String mediaType = 'image/jpeg'}) async { + // Save the file using the AttachmentQueue API + return await attachmentQueue.saveFile( + data: photoData, + mediaType: mediaType, + fileExtension: 'jpg', + metaData: 'Photo attachment for todo: $todoId', + updateHook: (context, attachment) async { + // Update the todo item to reference this attachment + await context.execute( + 'UPDATE todos SET photo_id = ? WHERE id = ?', + [attachment.id, todoId], + ); + }, + ); +} +``` + +## Deleting attachments + +To delete attachments, it is sufficient to stop referencing them in the data model, e.g. via +`UPDATE todos SET photo_id = NULL` in this example. The attachment sync implementation will eventually +delete orphaned attachments from the local storage. diff --git a/packages/powersync_core/example/README.md b/packages/powersync_core/example/README.md new file mode 100644 index 00000000..8dbcfb60 --- /dev/null +++ b/packages/powersync_core/example/README.md @@ -0,0 +1,3 @@ +# Examples + +- [Getting started](./getting_started.dart) diff --git a/packages/powersync_core/example/getting_started.dart b/packages/powersync_core/example/getting_started.dart new file mode 100644 index 00000000..13f43642 --- /dev/null +++ b/packages/powersync_core/example/getting_started.dart @@ -0,0 +1,109 @@ +import 'dart:ffi'; +import 'dart:io'; + +import 'package:powersync_core/powersync_core.dart'; +import 'package:path/path.dart'; +import 'package:powersync_core/sqlite3.dart' as sqlite; +import 'package:powersync_core/sqlite3_common.dart'; +import 'package:powersync_core/sqlite_async.dart'; +import 'package:powersync_core/sqlite3_open.dart' as sqlite_open; + +const schema = Schema([ + Table('customers', [Column.text('name'), Column.text('email')]) +]); + +late PowerSyncDatabase db; + +// Setup connector to backend if you would like to sync data. +class BackendConnector extends PowerSyncBackendConnector { + PowerSyncDatabase db; + + BackendConnector(this.db); + @override + // ignore: body_might_complete_normally_nullable + Future fetchCredentials() async { + // implement fetchCredentials + } + @override + Future uploadData(PowerSyncDatabase database) async { + // implement uploadData + } +} + +/// Custom factory to load the PowerSync extension. +/// This is required to load the extension from a custom location. +/// The extension is required to sync data with the backend. +/// On macOS and Linux, the default sqlite3 library is overridden to load the extension. +class PowerSyncDartOpenFactory extends PowerSyncOpenFactory { + PowerSyncDartOpenFactory({required super.path, super.sqliteOptions}); + + @override + CommonDatabase open(SqliteOpenOptions options) { + sqlite_open.open.overrideFor(sqlite_open.OperatingSystem.linux, () { + return DynamicLibrary.open('libsqlite3.so.0'); + }); + sqlite_open.open.overrideFor(sqlite_open.OperatingSystem.macOS, () { + return DynamicLibrary.open('libsqlite3.dylib'); + }); + return super.open(options); + } + + @override + void enableExtension() { + var powersyncLib = DynamicLibrary.open(getLibraryForPlatform()); + sqlite.sqlite3.ensureExtensionLoaded(sqlite.SqliteExtension.inLibrary( + powersyncLib, 'sqlite3_powersync_init')); + } + + @override + String getLibraryForPlatform({String? path = "."}) { + switch (Abi.current()) { + case Abi.androidArm: + case Abi.androidArm64: + case Abi.androidX64: + return '$path/libpowersync.so'; + case Abi.macosArm64: + case Abi.macosX64: + return '$path/libpowersync.dylib'; + case Abi.linuxX64: + case Abi.linuxArm64: + return '$path/libpowersync.so'; + case Abi.windowsX64: + return '$path/powersync.dll'; + case Abi.androidIA32: + throw PowersyncNotReadyException( + 'Unsupported processor architecture. X86 Android emulators are not ' + 'supported. Please use an x86_64 emulator instead. All physical ' + 'Android devices are supported including 32bit ARM.', + ); + default: + throw PowersyncNotReadyException( + 'Unsupported processor architecture "${Abi.current()}". ' + 'Please open an issue on GitHub to request it.', + ); + } + } +} + +Future getDatabasePath() async { + const dbFilename = 'powersync-demo.db'; + final dir = (Directory.current.uri).toFilePath(); + return join(dir, dbFilename); +} + +Future openDatabase() async { + // Setup the database. + final psFactory = PowerSyncDartOpenFactory(path: await getDatabasePath()); + db = PowerSyncDatabase.withFactory(psFactory, schema: schema); + + // Initialise the database. + await db.initialize(); + + // Run local statements. + await db.execute( + 'INSERT INTO customers(id, name, email) VALUES(uuid(), ?, ?)', + ['Fred', 'fred@example.org']); + + // Connect to backend + db.connect(connector: BackendConnector(db)); +} diff --git a/packages/powersync_core/lib/attachments/attachments.dart b/packages/powersync_core/lib/attachments/attachments.dart new file mode 100644 index 00000000..a69f9409 --- /dev/null +++ b/packages/powersync_core/lib/attachments/attachments.dart @@ -0,0 +1,12 @@ +/// Imports for attachments that are available on all platforms. +/// +/// For more details on using attachments, see the documentation for the topic. +/// +/// {@category attachments} +library; + +export '../src/attachments/attachment.dart'; +export '../src/attachments/attachment_queue_service.dart'; +export '../src/attachments/local_storage.dart'; +export '../src/attachments/remote_storage.dart'; +export '../src/attachments/sync_error_handler.dart'; diff --git a/packages/powersync_core/lib/attachments/io.dart b/packages/powersync_core/lib/attachments/io.dart new file mode 100644 index 00000000..142abb26 --- /dev/null +++ b/packages/powersync_core/lib/attachments/io.dart @@ -0,0 +1,12 @@ +/// A platform-specific import supporting attachments on native platforms. +/// +/// This library exports the [IOLocalStorage] class, implementing the +/// [LocalStorage] interface by storing files under a root directory. +/// +/// {@category attachments} +library; + +import '../src/attachments/io_local_storage.dart'; +import '../src/attachments/local_storage.dart'; + +export '../src/attachments/io_local_storage.dart'; diff --git a/packages/powersync_core/lib/powersync_core.dart b/packages/powersync_core/lib/powersync_core.dart new file mode 100644 index 00000000..ef2e97c7 --- /dev/null +++ b/packages/powersync_core/lib/powersync_core.dart @@ -0,0 +1,17 @@ +/// PowerSync Dart SDK. +/// +/// Use [PowerSyncDatabase] to open a database. +library; + +export 'src/connector.dart'; +export 'src/crud.dart'; +export 'src/database/powersync_database.dart'; +export 'src/exceptions.dart'; +export 'src/log.dart'; +export 'src/open_factory.dart'; +export 'src/schema.dart'; +export 'src/sync/options.dart' hide ResolvedSyncOptions; +export 'src/sync/stream.dart' hide CoreActiveStreamSubscription; +export 'src/sync/sync_status.dart' + hide BucketProgress, InternalSyncDownloadProgress, InternalSyncStatusAccess; +export 'src/uuid.dart'; diff --git a/packages/powersync_core/lib/sqlite3.dart b/packages/powersync_core/lib/sqlite3.dart new file mode 100644 index 00000000..66f87ad4 --- /dev/null +++ b/packages/powersync_core/lib/sqlite3.dart @@ -0,0 +1,5 @@ +/// Re-exports [sqlite3](https://pub.dev/packages/sqlite3) to expose sqlite3 without +/// adding it as a direct dependency. +library; + +export 'package:sqlite_async/sqlite3.dart'; diff --git a/packages/powersync_core/lib/sqlite3_common.dart b/packages/powersync_core/lib/sqlite3_common.dart new file mode 100644 index 00000000..df84a8e0 --- /dev/null +++ b/packages/powersync_core/lib/sqlite3_common.dart @@ -0,0 +1,5 @@ +/// Re-exports [sqlite3_common](https://pub.dev/packages/sqlite3) to expose sqlite3_common without +/// adding it as a direct dependency. +library; + +export 'package:sqlite_async/sqlite3_common.dart'; diff --git a/packages/powersync_core/lib/sqlite3_open.dart b/packages/powersync_core/lib/sqlite3_open.dart new file mode 100644 index 00000000..b5659245 --- /dev/null +++ b/packages/powersync_core/lib/sqlite3_open.dart @@ -0,0 +1,5 @@ +/// Re-exports [sqlite3_open](https://pub.dev/packages/sqlite3) to expose sqlite3_open without +/// adding it as a direct dependency. +library; + +export 'package:sqlite_async/sqlite3_open.dart'; diff --git a/packages/powersync_core/lib/sqlite_async.dart b/packages/powersync_core/lib/sqlite_async.dart new file mode 100644 index 00000000..1d09936d --- /dev/null +++ b/packages/powersync_core/lib/sqlite_async.dart @@ -0,0 +1,5 @@ +/// Re-exports [sqlite_async](https://pub.dev/packages/sqlite_async) to expose sqlite_async without +/// adding it as a direct dependency. +library; + +export 'package:sqlite_async/sqlite_async.dart'; diff --git a/packages/powersync/lib/src/abort_controller.dart b/packages/powersync_core/lib/src/abort_controller.dart similarity index 91% rename from packages/powersync/lib/src/abort_controller.dart rename to packages/powersync_core/lib/src/abort_controller.dart index 3cacc39b..d78da3bf 100644 --- a/packages/powersync/lib/src/abort_controller.dart +++ b/packages/powersync_core/lib/src/abort_controller.dart @@ -14,6 +14,10 @@ class AbortController { return _abortRequested.future; } + Future get onCompletion { + return _abortCompleter.future; + } + /// Abort, and wait until aborting is complete. Future abort() async { aborted = true; @@ -21,7 +25,7 @@ class AbortController { _abortRequested.complete(); } - await _abortCompleter.future; + await onCompletion; } /// Signal that an abort has completed. diff --git a/packages/powersync_core/lib/src/attachments/attachment.dart b/packages/powersync_core/lib/src/attachments/attachment.dart new file mode 100644 index 00000000..00f0032a --- /dev/null +++ b/packages/powersync_core/lib/src/attachments/attachment.dart @@ -0,0 +1,197 @@ +/// Defines attachment states and the Attachment model for the PowerSync +/// attachments system. +/// +/// Includes metadata, state, and utility methods for working with attachments. +library; + +import 'package:meta/meta.dart'; +import 'package:powersync_core/sqlite3_common.dart' show Row; +import 'package:powersync_core/powersync_core.dart'; + +/// Represents the state of an attachment. +/// +/// {@category attachments} +@experimental +enum AttachmentState { + /// The attachment is queued for download from the remote storage. + queuedDownload, + + /// The attachment is queued for upload to the remote storage. + queuedUpload, + + /// The attachment is queued for deletion from the remote storage. + queuedDelete, + + /// The attachment is fully synchronized with the remote storage. + synced, + + /// The attachment is archived and no longer actively synchronized. + archived; + + /// Constructs an [AttachmentState] from the corresponding integer value. + /// + /// Throws [ArgumentError] if the value does not match any [AttachmentState]. + static AttachmentState fromInt(int value) { + if (value < 0 || value >= AttachmentState.values.length) { + throw ArgumentError('Invalid value for AttachmentState: $value'); + } + return AttachmentState.values[value]; + } + + /// Returns the ordinal value of this [AttachmentState]. + int toInt() => index; +} + +/// Represents an attachment with metadata and state information. +/// +/// {@category Attachments} +/// +/// Properties: +/// - [id]: Unique identifier for the attachment. +/// - [timestamp]: Timestamp of the last record update. +/// - [filename]: Name of the attachment file, e.g., `[id].jpg`. +/// - [state]: Current state of the attachment, represented as an ordinal of [AttachmentState]. +/// - [localUri]: Local URI pointing to the attachment file, if available. +/// - [mediaType]: Media type of the attachment, typically represented as a MIME type. +/// - [size]: Size of the attachment in bytes, if available. +/// - [hasSynced]: Indicates whether the attachment has been synced locally before. +/// - [metaData]: Additional metadata associated with the attachment. +/// +/// {@category attachments} +@experimental +final class Attachment { + /// Unique identifier for the attachment. + final String id; + + /// Timestamp of the last record update. + final int timestamp; + + /// Name of the attachment file, e.g., `[id].jpg`. + final String filename; + + /// Current state of the attachment, represented as an ordinal of [AttachmentState]. + final AttachmentState state; + + /// Local URI pointing to the attachment file, if available. + final String? localUri; + + /// Media type of the attachment, typically represented as a MIME type. + final String? mediaType; + + /// Size of the attachment in bytes, if available. + final int? size; + + /// Indicates whether the attachment has been synced locally before. + final bool hasSynced; + + /// Additional metadata associated with the attachment. + final String? metaData; + + /// Creates an [Attachment] instance. + const Attachment({ + required this.id, + this.timestamp = 0, + required this.filename, + this.state = AttachmentState.queuedDownload, + this.localUri, + this.mediaType, + this.size, + this.hasSynced = false, + this.metaData, + }); + + /// Creates an [Attachment] instance from a database row. + /// + /// [row]: The [Row] containing attachment data. + /// Returns an [Attachment] instance populated with data from the row. + factory Attachment.fromRow(Row row) { + return Attachment( + id: row['id'] as String, + timestamp: row['timestamp'] as int? ?? 0, + filename: row['filename'] as String, + localUri: row['local_uri'] as String?, + mediaType: row['media_type'] as String?, + size: row['size'] as int?, + state: AttachmentState.fromInt(row['state'] as int), + hasSynced: (row['has_synced'] as int? ?? 0) > 0, + metaData: row['meta_data']?.toString(), + ); + } + + /// Returns a copy of this attachment with the given fields replaced. + Attachment copyWith({ + String? id, + int? timestamp, + String? filename, + AttachmentState? state, + String? localUri, + String? mediaType, + int? size, + bool? hasSynced, + String? metaData, + }) { + return Attachment( + id: id ?? this.id, + timestamp: timestamp ?? this.timestamp, + filename: filename ?? this.filename, + state: state ?? this.state, + localUri: localUri ?? this.localUri, + mediaType: mediaType ?? this.mediaType, + size: size ?? this.size, + hasSynced: hasSynced ?? this.hasSynced, + metaData: metaData ?? this.metaData, + ); + } + + Attachment markAsUnavailableLocally(AttachmentState newState) { + return Attachment( + id: id, + timestamp: timestamp, + filename: filename, + state: newState, + localUri: null, + mediaType: mediaType, + size: size, + hasSynced: false, + metaData: metaData, + ); + } + + @override + String toString() { + return 'Attachment(id: $id, state: $state, localUri: $localUri, metadata: $metaData)'; + } +} + +/// Table definition for the attachments queue. +/// +/// The columns in this table are used by the attachments implementation to +/// store which attachments have been download and tracks metadata for state. +/// +/// {@category attachments} +@experimental +final class AttachmentsQueueTable extends Table { + AttachmentsQueueTable({ + String attachmentsQueueTableName = defaultTableName, + List additionalColumns = const [], + List indexes = const [], + String? viewName, + }) : super.localOnly( + attachmentsQueueTableName, + [ + const Column.text('filename'), + const Column.text('local_uri'), + const Column.integer('timestamp'), + const Column.integer('size'), + const Column.text('media_type'), + const Column.integer('state'), + const Column.integer('has_synced'), + const Column.text('meta_data'), + ...additionalColumns, + ], + viewName: viewName, + indexes: indexes, + ); + + static const defaultTableName = 'attachments_queue'; +} diff --git a/packages/powersync_core/lib/src/attachments/attachment_queue_service.dart b/packages/powersync_core/lib/src/attachments/attachment_queue_service.dart new file mode 100644 index 00000000..0ec673e0 --- /dev/null +++ b/packages/powersync_core/lib/src/attachments/attachment_queue_service.dart @@ -0,0 +1,443 @@ +// Implements the attachment queue for PowerSync attachments. +// +// This class manages the lifecycle of attachment records, including watching for new attachments, +// syncing with remote storage, handling uploads, downloads, and deletes, and managing local storage. +// It provides hooks for error handling, cache management, and custom filename resolution. + +import 'dart:async'; + +import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; +import 'package:powersync_core/powersync_core.dart'; +import 'package:sqlite_async/sqlite_async.dart'; + +import 'attachment.dart'; +import 'implementations/attachment_context.dart'; +import 'local_storage.dart'; +import 'remote_storage.dart'; +import 'sync_error_handler.dart'; +import 'implementations/attachment_service.dart'; +import 'sync/syncing_service.dart'; + +/// A watched attachment record item. +/// +/// This is usually returned from watching all relevant attachment IDs. +/// +/// - [id]: Id for the attachment record. +/// - [fileExtension]: File extension used to determine an internal filename for storage if no [filename] is provided. +/// - [filename]: Filename to store the attachment with. +/// - [metaData]: Optional metadata for the attachment record. +/// +/// {@category attachments} +@experimental +final class WatchedAttachmentItem { + /// Id for the attachment record. + final String id; + + /// File extension used to determine an internal filename for storage if no [filename] is provided. + final String? fileExtension; + + /// Filename to store the attachment with. + final String? filename; + + /// Optional metadata for the attachment record. + final String? metaData; + + /// Creates a [WatchedAttachmentItem]. + /// + /// Either [fileExtension] or [filename] must be provided. + const WatchedAttachmentItem({ + required this.id, + this.fileExtension, + this.filename, + this.metaData, + }) : assert( + fileExtension != null || filename != null, + 'Either fileExtension or filename must be provided.', + ); +} + +/// Class used to implement the attachment queue. +/// +/// Manages the lifecycle of attachment records, including watching for new attachments, +/// syncing with remote storage, handling uploads, downloads, and deletes, and managing local storage. +/// +/// {@category attachments} +@experimental +base class AttachmentQueue { + final PowerSyncDatabase _db; + final Stream> Function() _watchAttachments; + final LocalStorage _localStorage; + final bool _downloadAttachments; + final Logger _logger; + + final Mutex _mutex = Mutex(); + bool _closed = false; + StreamSubscription? _syncStatusSubscription; + StreamSubscription? _watchedAttachmentsSubscription; + final AttachmentService _attachmentsService; + final SyncingService _syncingService; + + AttachmentQueue._( + {required PowerSyncDatabase db, + required Stream> Function() watchAttachments, + required LocalStorage localStorage, + required bool downloadAttachments, + required Logger logger, + required AttachmentService attachmentsService, + required SyncingService syncingService}) + : _db = db, + _watchAttachments = watchAttachments, + _localStorage = localStorage, + _downloadAttachments = downloadAttachments, + _logger = logger, + _attachmentsService = attachmentsService, + _syncingService = syncingService; + + /// Creates a new attachment queue. + /// + /// Parameters: + /// + /// - [db]: PowerSync database client. + /// - [remoteStorage]: Adapter which interfaces with the remote storage backend. + /// - [watchAttachments]: A stream generator for the current state of local attachments. + /// - [localStorage]: Provides access to local filesystem storage methods. + /// - [attachmentsQueueTableName]: SQLite table where attachment state will be recorded. + /// - [errorHandler]: Attachment operation error handler. Specifies if failed attachment operations should be retried. + /// - [syncInterval]: Periodic interval to trigger attachment sync operations. + /// - [archivedCacheLimit]: Defines how many archived records are retained as a cache. + /// - [syncThrottleDuration]: Throttles remote sync operations triggering. + /// - [downloadAttachments]: Should attachments be downloaded. + /// - [logger]: Logging interface used for all log operations. + factory AttachmentQueue({ + required PowerSyncDatabase db, + required RemoteStorage remoteStorage, + required Stream> Function() watchAttachments, + required LocalStorage localStorage, + String attachmentsQueueTableName = AttachmentsQueueTable.defaultTableName, + AttachmentErrorHandler? errorHandler, + Duration syncInterval = const Duration(seconds: 30), + int archivedCacheLimit = 100, + Duration syncThrottleDuration = const Duration(seconds: 1), + bool downloadAttachments = true, + Logger? logger, + }) { + final resolvedLogger = logger ?? db.logger; + + final attachmentsService = AttachmentService( + db: db, + logger: resolvedLogger, + maxArchivedCount: archivedCacheLimit, + attachmentsQueueTableName: attachmentsQueueTableName, + ); + final syncingService = SyncingService( + remoteStorage: remoteStorage, + localStorage: localStorage, + attachmentsService: attachmentsService, + errorHandler: errorHandler, + syncThrottle: syncThrottleDuration, + period: syncInterval, + logger: resolvedLogger, + ); + + return AttachmentQueue._( + db: db, + watchAttachments: watchAttachments, + localStorage: localStorage, + downloadAttachments: downloadAttachments, + logger: resolvedLogger, + attachmentsService: attachmentsService, + syncingService: syncingService, + ); + } + + /// Initialize the attachment queue by: + /// 1. Creating the attachments directory. + /// 2. Adding watches for uploads, downloads, and deletes. + /// 3. Adding a trigger to run uploads, downloads, and deletes when the device is online after being offline. + Future startSync() async { + await _mutex.lock(() async { + if (_closed) { + throw StateError('Attachment queue has been closed'); + } + + await _stopSyncingInternal(); + + await _localStorage.initialize(); + + await _attachmentsService.withContext((context) async { + await _verifyAttachments(context); + }); + + // Listen for connectivity changes and watched attachments + await _syncingService.startSync(); + + _watchedAttachmentsSubscription = + _watchAttachments().listen((items) async { + await _processWatchedAttachments(items); + }); + + var previouslyConnected = _db.currentStatus.connected; + _syncStatusSubscription = _db.statusStream.listen((status) { + if (!previouslyConnected && status.connected) { + _syncingService.triggerSync(); + } + + previouslyConnected = status.connected; + }); + _watchAttachments().listen((items) async { + await _processWatchedAttachments(items); + }); + + _logger.info('AttachmentQueue started syncing.'); + }); + } + + /// Stops syncing. Syncing may be resumed with [startSync]. + Future stopSyncing() async { + await _mutex.lock(() async { + await _stopSyncingInternal(); + }); + } + + Future _stopSyncingInternal() async { + if (_closed || + _syncStatusSubscription == null || + _watchedAttachmentsSubscription == null) { + return; + } + + await ( + _syncStatusSubscription!.cancel(), + _watchedAttachmentsSubscription!.cancel(), + ).wait; + + _syncStatusSubscription = null; + _watchedAttachmentsSubscription = null; + await _syncingService.stopSync(); + + _logger.info('AttachmentQueue stopped syncing.'); + } + + /// Closes the queue. The queue cannot be used after closing. + Future close() async { + await _mutex.lock(() async { + if (_closed) return; + + await _stopSyncingInternal(); + _closed = true; + _logger.info('AttachmentQueue closed.'); + }); + } + + /// Resolves the filename for new attachment items. + /// Concatenates the attachment ID and extension by default. + Future resolveNewAttachmentFilename( + String attachmentId, + String? fileExtension, + ) async { + return '$attachmentId.${fileExtension ?? 'dat'}'; + } + + /// Processes attachment items returned from `watchAttachments`. + /// + /// The default implementation asserts the items returned from + /// `watchAttachments` as the definitive state for local attachments. + Future _processWatchedAttachments( + List items, + ) async { + await _attachmentsService.withContext((context) async { + final currentAttachments = await context.getAttachments(); + final List attachmentUpdates = []; + + for (final item in items) { + final existingQueueItem = + currentAttachments.where((a) => a.id == item.id).firstOrNull; + + if (existingQueueItem == null) { + if (!_downloadAttachments) continue; + + // This item should be added to the queue. + // This item is assumed to be coming from an upstream sync. + final String filename = item.filename ?? + await resolveNewAttachmentFilename(item.id, item.fileExtension); + + attachmentUpdates.add( + Attachment( + id: item.id, + filename: filename, + state: AttachmentState.queuedDownload, + metaData: item.metaData, + ), + ); + } else if (existingQueueItem.state == AttachmentState.archived) { + // The attachment is present again. Need to queue it for sync. + if (existingQueueItem.hasSynced) { + // No remote action required, we can restore the record (avoids deletion). + attachmentUpdates.add( + existingQueueItem.copyWith(state: AttachmentState.synced), + ); + } else { + // The localURI should be set if the record was meant to be downloaded + // and has been synced. If it's missing and hasSynced is false then + // it must be an upload operation. + attachmentUpdates.add( + existingQueueItem.copyWith( + state: existingQueueItem.localUri == null + ? AttachmentState.queuedDownload + : AttachmentState.queuedUpload, + ), + ); + } + } + } + + // Archive any items not specified in the watched items. + // For queuedDelete or queuedUpload states, archive only if hasSynced is true. + // For other states, archive if the record is not found in the items. + for (final attachment in currentAttachments) { + final notInWatchedItems = items.every( + (update) => update.id != attachment.id, + ); + + if (notInWatchedItems) { + switch (attachment.state) { + case AttachmentState.queuedDelete: + case AttachmentState.queuedUpload: + if (attachment.hasSynced) { + attachmentUpdates.add( + attachment.copyWith(state: AttachmentState.archived), + ); + } + default: + attachmentUpdates.add( + attachment.copyWith(state: AttachmentState.archived), + ); + } + } + } + + await context.saveAttachments(attachmentUpdates); + }); + } + + /// Generates a random attachment id. + Future generateAttachmentId() async { + final row = await _db.get('SELECT uuid() as id'); + return row['id'] as String; + } + + /// Creates a new attachment locally and queues it for upload. + /// The filename is resolved using [resolveNewAttachmentFilename]. + Future saveFile({ + required Stream> data, + required String mediaType, + String? fileExtension, + String? metaData, + String? id, + required Future Function( + SqliteWriteContext context, Attachment attachment) + updateHook, + }) async { + final resolvedId = id ?? await generateAttachmentId(); + + final filename = await resolveNewAttachmentFilename( + resolvedId, + fileExtension, + ); + + // Write the file to the filesystem. + final fileSize = await _localStorage.saveFile(filename, data); + + return await _attachmentsService.withContext((attachmentContext) async { + return await _db.writeTransaction((tx) async { + final attachment = Attachment( + id: resolvedId, + filename: filename, + size: fileSize, + mediaType: mediaType, + state: AttachmentState.queuedUpload, + localUri: filename, + metaData: metaData, + ); + + // Allow consumers to set relationships to this attachment ID. + await updateHook(tx, attachment); + + return await attachmentContext.upsertAttachment(attachment, tx); + }); + }); + } + + /// Queues an attachment for delete. + /// The default implementation assumes the attachment record already exists locally. + Future deleteFile({ + required String attachmentId, + required Future Function( + SqliteWriteContext context, Attachment attachment) + updateHook, + }) async { + return await _attachmentsService.withContext((attachmentContext) async { + final attachment = await attachmentContext.getAttachment(attachmentId); + if (attachment == null) { + throw Exception( + 'Attachment record with id $attachmentId was not found.', + ); + } + + return await _db.writeTransaction((tx) async { + await updateHook(tx, attachment); + return await attachmentContext.upsertAttachment( + attachment.copyWith( + state: AttachmentState.queuedDelete, + hasSynced: false, + ), + tx, + ); + }); + }); + } + + /// Removes all archived items. + Future expireCache() async { + await _attachmentsService.withContext((context) async { + bool done; + do { + done = await _syncingService.deleteArchivedAttachments(context); + } while (!done); + }); + } + + /// Clears the attachment queue and deletes all attachment files. + Future clearQueue() async { + await _attachmentsService.withContext((context) async { + await context.clearQueue(); + }); + await _localStorage.clear(); + } + + /// Cleans up stale attachments. + Future _verifyAttachments(AttachmentContext context) async { + final attachments = await context.getActiveAttachments(); + final List updates = []; + + for (final attachment in attachments) { + // Only check attachments that should have local files + if (attachment.localUri == null) { + // Skip attachments that don't have localUri (like queued downloads) + continue; + } + + final exists = await _localStorage.fileExists(attachment.localUri!); + if ((attachment.state == AttachmentState.synced || + attachment.state == AttachmentState.queuedUpload) && + !exists) { + updates.add( + attachment.markAsUnavailableLocally(AttachmentState.archived), + ); + } + } + + await context.saveAttachments(updates); + } +} diff --git a/packages/powersync_core/lib/src/attachments/implementations/attachment_context.dart b/packages/powersync_core/lib/src/attachments/implementations/attachment_context.dart new file mode 100644 index 00000000..9428d79c --- /dev/null +++ b/packages/powersync_core/lib/src/attachments/implementations/attachment_context.dart @@ -0,0 +1,160 @@ +import 'package:powersync_core/powersync_core.dart'; +import 'package:powersync_core/sqlite3_common.dart'; +import 'package:logging/logging.dart'; +import 'package:sqlite_async/sqlite_async.dart'; +import 'package:meta/meta.dart'; + +import '../attachment.dart'; + +@internal +final class AttachmentContext { + final PowerSyncDatabase db; + final Logger log; + final int maxArchivedCount; + final String attachmentsQueueTableName; + + AttachmentContext( + this.db, + this.log, + this.maxArchivedCount, + this.attachmentsQueueTableName, + ); + + /// Table used for storing attachments in the attachment queue. + String get table { + return attachmentsQueueTableName; + } + + Future deleteAttachment(String id) async { + log.info('deleteAttachment: $id'); + await db.writeTransaction((tx) async { + await tx.execute('DELETE FROM $table WHERE id = ?', [id]); + }); + } + + Future ignoreAttachment(String id) async { + await db.execute( + 'UPDATE $table SET state = ${AttachmentState.archived.index} WHERE id = ?', + [id], + ); + } + + Future getAttachment(String id) async { + final row = await db.getOptional('SELECT * FROM $table WHERE id = ?', [id]); + if (row == null) { + return null; + } + return Attachment.fromRow(row); + } + + Future saveAttachment(Attachment attachment) async { + return await db.writeLock((ctx) async { + return await upsertAttachment(attachment, ctx); + }); + } + + Future saveAttachments(List attachments) async { + if (attachments.isEmpty) { + log.finer('No attachments to save.'); + return; + } + await db.writeTransaction((tx) async { + for (final attachment in attachments) { + await upsertAttachment(attachment, tx); + } + }); + } + + Future> getAttachmentIds() async { + ResultSet results = await db.getAll( + 'SELECT id FROM $table WHERE id IS NOT NULL', + ); + + List ids = results.map((row) => row['id'] as String).toList(); + + return ids; + } + + Future> getAttachments() async { + final results = await db.getAll('SELECT * FROM $table'); + return results.map((row) => Attachment.fromRow(row)).toList(); + } + + Future> getActiveAttachments() async { + // Return all attachments that are not archived (i.e., state != AttachmentState.archived) + final results = await db.getAll('SELECT * FROM $table WHERE state != ?', [ + AttachmentState.archived.index, + ]); + return results.map((row) => Attachment.fromRow(row)).toList(); + } + + Future clearQueue() async { + log.info('Clearing attachment queue...'); + await db.execute('DELETE FROM $table'); + } + + Future deleteArchivedAttachments( + Future Function(List) callback, + ) async { + // Only delete archived attachments exceeding the maxArchivedCount, ordered by timestamp DESC + const limit = 1000; + + final results = await db.getAll( + 'SELECT * FROM $table WHERE state = ? ORDER BY timestamp DESC LIMIT ? OFFSET ?', + [ + AttachmentState.archived.index, + limit, + maxArchivedCount, + ], + ); + final archivedAttachments = + results.map((row) => Attachment.fromRow(row)).toList(); + + if (archivedAttachments.isEmpty) { + return false; + } + + log.info( + 'Deleting ${archivedAttachments.length} archived attachments (exceeding maxArchivedCount=$maxArchivedCount)...'); + // Call the callback with the list of archived attachments before deletion + await callback(archivedAttachments); + + // Delete the archived attachments from the table + final ids = archivedAttachments.map((a) => a.id).toList(); + if (ids.isNotEmpty) { + await db.executeBatch('DELETE FROM $table WHERE id = ?', [ + for (final id in ids) [id], + ]); + } + + log.info('Deleted ${archivedAttachments.length} archived attachments.'); + return archivedAttachments.length < limit; + } + + Future upsertAttachment( + Attachment attachment, + SqliteWriteContext context, + ) async { + log.finest('Updating attachment ${attachment.id}: ${attachment.state}'); + + await context.execute( + '''INSERT OR REPLACE INTO + $table (id, timestamp, filename, local_uri, media_type, size, state, has_synced, meta_data) + VALUES + (?, ?, ?, ?, ?, ?, ?, ?, ?)''', + [ + attachment.id, + attachment.timestamp, + attachment.filename, + attachment.localUri, + attachment.mediaType, + attachment.size, + attachment.state.index, + attachment.hasSynced ? 1 : 0, + attachment.metaData, + ], + ); + + return attachment; + } +} diff --git a/packages/powersync_core/lib/src/attachments/implementations/attachment_service.dart b/packages/powersync_core/lib/src/attachments/implementations/attachment_service.dart new file mode 100644 index 00000000..0ccafd75 --- /dev/null +++ b/packages/powersync_core/lib/src/attachments/implementations/attachment_service.dart @@ -0,0 +1,75 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:logging/logging.dart'; +import 'package:powersync_core/powersync_core.dart'; +import 'package:sqlite_async/sqlite_async.dart'; + +import '../attachment.dart'; +import 'attachment_context.dart'; + +@internal +final class AttachmentService { + final PowerSyncDatabase db; + final Logger logger; + final int maxArchivedCount; + final String attachmentsQueueTableName; + final Mutex _mutex = Mutex(); + + late final AttachmentContext _context; + + AttachmentService({ + required this.db, + required this.logger, + required this.maxArchivedCount, + required this.attachmentsQueueTableName, + }) { + _context = AttachmentContext( + db, + logger, + maxArchivedCount, + attachmentsQueueTableName, + ); + } + + Stream watchActiveAttachments({Duration? throttle}) async* { + logger.info('Watching attachments...'); + + // Watch for attachments with active states (queued for upload, download, or delete) + final stream = db.watch( + ''' + SELECT + id + FROM + $attachmentsQueueTableName + WHERE + state = ? + OR state = ? + OR state = ? + ORDER BY + timestamp ASC + ''', + parameters: [ + AttachmentState.queuedUpload.index, + AttachmentState.queuedDownload.index, + AttachmentState.queuedDelete.index, + ], + throttle: throttle ?? const Duration(milliseconds: 30), + ); + + yield* stream; + } + + Future withContext( + Future Function(AttachmentContext ctx) action, + ) async { + return await _mutex.lock(() async { + try { + return await action(_context); + } catch (e, stackTrace) { + // Re-throw the error to be handled by the caller + Error.throwWithStackTrace(e, stackTrace); + } + }); + } +} diff --git a/packages/powersync_core/lib/src/attachments/io_local_storage.dart b/packages/powersync_core/lib/src/attachments/io_local_storage.dart new file mode 100644 index 00000000..67d1b578 --- /dev/null +++ b/packages/powersync_core/lib/src/attachments/io_local_storage.dart @@ -0,0 +1,93 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; + +import 'local_storage.dart'; + +/// Implements [LocalStorage] for device filesystem using Dart IO. +/// +/// Handles file and directory operations for attachments. The database only +/// stores relative paths for attachments that this implementation resolves +/// against the root path provided as a constructor argument. For that reason, +/// it's important that the root directory stays consistent, as data may be lost +/// otherwise. +/// +/// {@category attachments} +@experimental +final class IOLocalStorage implements LocalStorage { + final Directory _root; + + const IOLocalStorage(this._root); + + File _fileFor(String filePath) => File(p.join(_root.path, filePath)); + + @override + Future saveFile(String filePath, Stream> data) async { + final file = _fileFor(filePath); + await file.parent.create(recursive: true); + return (await data.pipe(_LengthTrackingSink(file.openWrite()))) as int; + } + + @override + Stream readFile(String filePath, {String? mediaType}) async* { + final file = _fileFor(filePath); + if (!await file.exists()) { + throw FileSystemException('File does not exist', filePath); + } + final source = file.openRead(); + await for (final chunk in source) { + yield chunk is Uint8List ? chunk : Uint8List.fromList(chunk); + } + } + + @override + Future deleteFile(String filePath) async { + final file = _fileFor(filePath); + if (await file.exists()) { + await file.delete(); + } + } + + @override + Future fileExists(String filePath) async { + return await _fileFor(filePath).exists(); + } + + /// Creates a directory and all necessary parent directories dynamically if they do not exist. + @override + Future initialize() async { + await _root.create(recursive: true); + } + + @override + Future clear() async { + if (await _root.exists()) { + await _root.delete(recursive: true); + } + await _root.create(recursive: true); + } +} + +final class _LengthTrackingSink implements StreamConsumer> { + final StreamConsumer> inner; + var bytesWritten = 0; + + _LengthTrackingSink(this.inner); + + @override + Future addStream(Stream> stream) { + return inner.addStream(stream.map((event) { + bytesWritten += event.length; + return event; + })); + } + + @override + Future close() async { + await inner.close(); + return bytesWritten; + } +} diff --git a/packages/powersync_core/lib/src/attachments/local_storage.dart b/packages/powersync_core/lib/src/attachments/local_storage.dart new file mode 100644 index 00000000..c43db1ef --- /dev/null +++ b/packages/powersync_core/lib/src/attachments/local_storage.dart @@ -0,0 +1,102 @@ +/// @docImport 'package:powersync_core/attachments/io.dart'; +library; + +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; + +/// An interface responsible for storing attachment data locally. +/// +/// This interface is only responsible for storing attachment content, +/// essentially acting as a key-value store of virtual paths to blobs. +/// +/// On native platforms, you can use the [IOLocalStorage] implemention. On the +/// web, no default implementation is available at the moment. +/// +/// {@category attachments} +@experimental +abstract interface class LocalStorage { + /// Returns an in-memory [LocalStorage] implementation, suitable for testing. + factory LocalStorage.inMemory() = _InMemoryStorage; + + /// Saves binary data stream to storage at the specified file path + /// + /// [filePath] - Path where the file will be stored + /// [data] - List of binary data to store + /// Returns the total size of the written data in bytes + Future saveFile(String filePath, Stream> data); + + /// Retrieves binary data stream from storage at the specified file path + /// + /// [filePath] - Path of the file to read + /// + /// Returns a stream of binary data + Stream readFile(String filePath); + + /// Deletes a file at the specified path + /// + /// [filePath] - Path of the file to delete + Future deleteFile(String filePath); + + /// Checks if a file exists at the specified path + /// + /// [filePath] - Path to check + /// + /// Returns true if the file exists, false otherwise + Future fileExists(String filePath); + + /// Initializes the storage, performing any necessary setup. + Future initialize(); + + /// Clears all data from the storage. + Future clear(); +} + +final class _InMemoryStorage implements LocalStorage { + final Map content = {}; + + String _keyForPath(String path) { + return p.normalize(path); + } + + @override + Future clear() async { + content.clear(); + } + + @override + Future deleteFile(String filePath) async { + content.remove(_keyForPath(filePath)); + } + + @override + Future fileExists(String filePath) async { + return content.containsKey(_keyForPath(filePath)); + } + + @override + Future initialize() async {} + + @override + Stream readFile(String filePath) { + return switch (content[_keyForPath(filePath)]) { + null => + Stream.error('file at $filePath does not exist in in-memory storage'), + final contents => Stream.value(contents), + }; + } + + @override + Future saveFile(String filePath, Stream> data) async { + var length = 0; + final builder = BytesBuilder(copy: false); + await for (final chunk in data) { + length += chunk.length; + builder.add(chunk); + } + + content[_keyForPath(filePath)] = builder.takeBytes(); + return length; + } +} diff --git a/packages/powersync_core/lib/src/attachments/remote_storage.dart b/packages/powersync_core/lib/src/attachments/remote_storage.dart new file mode 100644 index 00000000..35818b9b --- /dev/null +++ b/packages/powersync_core/lib/src/attachments/remote_storage.dart @@ -0,0 +1,34 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; + +import 'attachment.dart'; + +/// An interface responsible for uploading and downloading attachments from a +/// remote source, like e.g. S3 or Firebase cloud storage. +/// +/// {@category attachments} +@experimental +abstract interface class RemoteStorage { + /// Uploads a file to remote storage. + /// + /// [fileData] is a stream of byte arrays representing the file data. + /// [attachment] is the attachment record associated with the file. + Future uploadFile( + Stream fileData, + Attachment attachment, + ); + + /// Downloads a file from remote storage. + /// + /// [attachment] is the attachment record associated with the file. + /// + /// Returns a stream of byte arrays representing the file data. + Future>> downloadFile(Attachment attachment); + + /// Deletes a file from remote storage. + /// + /// [attachment] is the attachment record associated with the file. + Future deleteFile(Attachment attachment); +} diff --git a/packages/powersync_core/lib/src/attachments/sync/syncing_service.dart b/packages/powersync_core/lib/src/attachments/sync/syncing_service.dart new file mode 100644 index 00000000..4bc1a266 --- /dev/null +++ b/packages/powersync_core/lib/src/attachments/sync/syncing_service.dart @@ -0,0 +1,281 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:logging/logging.dart'; +import 'package:async/async.dart'; + +import '../attachment.dart'; +import '../implementations/attachment_context.dart'; +import '../implementations/attachment_service.dart'; +import '../local_storage.dart'; +import '../remote_storage.dart'; +import '../sync_error_handler.dart'; + +/// SyncingService is responsible for syncing attachments between local and remote storage. +/// +/// This service handles downloading, uploading, and deleting attachments, as well as +/// periodically syncing attachment states. It ensures proper lifecycle management +/// of sync operations and provides mechanisms for error handling and retries. +/// +/// Properties: +/// - [remoteStorage]: The remote storage implementation for handling file operations. +/// - [localStorage]: The local storage implementation for managing files locally. +/// - [attachmentsService]: The service for managing attachment states and operations. +/// - [errorHandler]: Optional error handler for managing sync-related errors. +@internal +final class SyncingService { + final RemoteStorage remoteStorage; + final LocalStorage localStorage; + final AttachmentService attachmentsService; + final AttachmentErrorHandler? errorHandler; + final Duration syncThrottle; + final Duration period; + final Logger logger; + + StreamSubscription? _syncSubscription; + StreamSubscription? _periodicSubscription; + bool _isClosed = false; + final _syncTriggerController = StreamController.broadcast(); + + SyncingService({ + required this.remoteStorage, + required this.localStorage, + required this.attachmentsService, + this.errorHandler, + this.syncThrottle = const Duration(seconds: 5), + this.period = const Duration(seconds: 30), + required this.logger, + }); + + /// Starts the syncing process, including periodic and event-driven sync operations. + Future startSync() async { + if (_isClosed) return; + + _syncSubscription?.cancel(); + _periodicSubscription?.cancel(); + + // Create a merged stream of manual triggers and attachment changes + final attachmentChanges = attachmentsService.watchActiveAttachments( + throttle: syncThrottle, + ); + final manualTriggers = _syncTriggerController.stream; + + late StreamSubscription sub; + final syncStream = + StreamGroup.merge([attachmentChanges, manualTriggers]) + .takeWhile((_) => sub == _syncSubscription) + .asyncMap((_) async { + await attachmentsService.withContext((context) async { + final attachments = await context.getActiveAttachments(); + logger.info('Found ${attachments.length} active attachments'); + await handleSync(attachments, context); + await deleteArchivedAttachments(context); + }); + }); + + _syncSubscription = sub = syncStream.listen(null); + + // Start periodic sync using instance period + _periodicSubscription = Stream.periodic(period, (_) {}).listen(( + _, + ) { + logger.info('Periodically syncing attachments'); + triggerSync(); + }); + } + + /// Enqueues a sync operation (manual trigger). + void triggerSync() { + if (!_isClosed) _syncTriggerController.add(null); + } + + /// Stops all ongoing sync operations. + Future stopSync() async { + await _periodicSubscription?.cancel(); + + final subscription = _syncSubscription; + // Add a trigger event after clearing the subscription, which will make + // the takeWhile() callback cancel. This allows us to use asFuture() here, + // ensuring that we only complete this future when the stream is actually + // done. + _syncSubscription = null; + _syncTriggerController.add(null); + await subscription?.asFuture(); + } + + /// Closes the syncing service, stopping all operations and releasing resources. + Future close() async { + _isClosed = true; + await stopSync(); + await _syncTriggerController.close(); + } + + /// Handles syncing operations for a list of attachments, including downloading, + /// uploading, and deleting files based on their states. + /// + /// [attachments]: The list of attachments to process. + /// [context]: The attachment context used for managing attachment states. + Future handleSync( + List attachments, + AttachmentContext context, + ) async { + logger.info('Starting handleSync with ${attachments.length} attachments'); + final updatedAttachments = []; + + for (final attachment in attachments) { + logger.info( + 'Processing attachment ${attachment.id} with state: ${attachment.state}', + ); + try { + switch (attachment.state) { + case AttachmentState.queuedDownload: + logger.info('Downloading [${attachment.filename}]'); + updatedAttachments.add(await downloadAttachment(attachment)); + break; + case AttachmentState.queuedUpload: + logger.info('Uploading [${attachment.filename}]'); + updatedAttachments.add(await uploadAttachment(attachment)); + break; + case AttachmentState.queuedDelete: + logger.info('Deleting [${attachment.filename}]'); + updatedAttachments.add(await deleteAttachment(attachment, context)); + break; + case AttachmentState.synced: + logger.info('Attachment ${attachment.id} is already synced'); + break; + case AttachmentState.archived: + logger.info('Attachment ${attachment.id} is archived'); + break; + } + } catch (e, st) { + logger.warning('Error during sync for ${attachment.id}', e, st); + } + } + + if (updatedAttachments.isNotEmpty) { + logger.info('Saving ${updatedAttachments.length} updated attachments'); + await context.saveAttachments(updatedAttachments); + } + } + + /// Uploads an attachment from local storage to remote storage. + /// + /// [attachment]: The attachment to upload. + /// Returns the updated attachment with its new state. + Future uploadAttachment(Attachment attachment) async { + logger.info('Starting upload for attachment ${attachment.id}'); + try { + if (attachment.localUri == null) { + throw Exception('No localUri for attachment $attachment'); + } + await remoteStorage.uploadFile( + localStorage.readFile(attachment.localUri!), + attachment, + ); + logger.info( + 'Successfully uploaded attachment "${attachment.id}" to Cloud Storage', + ); + return attachment.copyWith( + state: AttachmentState.synced, + hasSynced: true, + ); + } catch (e, st) { + logger.warning( + 'Upload attachment error for attachment $attachment', + e, + st, + ); + if (errorHandler != null) { + final shouldRetry = + await errorHandler!.onUploadError(attachment, e, st); + if (!shouldRetry) { + logger.info('Attachment with ID ${attachment.id} has been archived'); + return attachment.copyWith(state: AttachmentState.archived); + } + } + return attachment; + } + } + + /// Downloads an attachment from remote storage and saves it to local storage. + /// + /// [attachment]: The attachment to download. + /// Returns the updated attachment with its new state. + Future downloadAttachment(Attachment attachment) async { + logger.info('Starting download for attachment ${attachment.id}'); + final attachmentPath = attachment.filename; + try { + final fileStream = await remoteStorage.downloadFile(attachment); + await localStorage.saveFile(attachmentPath, fileStream); + logger.info('Successfully downloaded file "${attachment.id}"'); + + return attachment.copyWith( + localUri: attachmentPath, + state: AttachmentState.synced, + hasSynced: true, + ); + } catch (e, st) { + if (errorHandler != null) { + final shouldRetry = + await errorHandler!.onDownloadError(attachment, e, st); + if (!shouldRetry) { + logger.info('Attachment with ID ${attachment.id} has been archived'); + return attachment.copyWith(state: AttachmentState.archived); + } + } + logger.warning( + 'Download attachment error for attachment $attachment', + e, + st, + ); + return attachment; + } + } + + /// Deletes an attachment from remote and local storage, and removes it from the queue. + /// + /// [attachment]: The attachment to delete. + /// Returns the updated attachment with its new state. + Future deleteAttachment( + Attachment attachment, AttachmentContext context) async { + try { + logger.info('Deleting attachment ${attachment.id} from remote storage'); + await remoteStorage.deleteFile(attachment); + + if (attachment.localUri != null && + await localStorage.fileExists(attachment.localUri!)) { + await localStorage.deleteFile(attachment.localUri!); + } + // Remove the attachment record from the queue in a transaction. + await context.deleteAttachment(attachment.id); + return attachment.copyWith(state: AttachmentState.archived); + } catch (e, st) { + if (errorHandler != null) { + final shouldRetry = + await errorHandler!.onDeleteError(attachment, e, st); + if (!shouldRetry) { + logger.info('Attachment with ID ${attachment.id} has been archived'); + return attachment.copyWith(state: AttachmentState.archived); + } + } + logger.warning('Error deleting attachment: $e', e, st); + return attachment; + } + } + + /// Deletes archived attachments from local storage. + /// + /// [context]: The attachment context used to retrieve and manage archived attachments. + /// Returns `true` if all archived attachments were successfully deleted, `false` otherwise. + Future deleteArchivedAttachments( + AttachmentContext context, + ) async { + return context.deleteArchivedAttachments((pendingDelete) async { + for (final attachment in pendingDelete) { + if (attachment.localUri == null) continue; + if (!await localStorage.fileExists(attachment.localUri!)) continue; + await localStorage.deleteFile(attachment.localUri!); + } + }); + } +} diff --git a/packages/powersync_core/lib/src/attachments/sync_error_handler.dart b/packages/powersync_core/lib/src/attachments/sync_error_handler.dart new file mode 100644 index 00000000..30aafcf4 --- /dev/null +++ b/packages/powersync_core/lib/src/attachments/sync_error_handler.dart @@ -0,0 +1,102 @@ +import 'package:meta/meta.dart'; + +import 'attachment.dart'; + +/// The signature of a function handling an exception when uploading, +/// downloading or deleting an exception. +/// +/// It returns `true` if the operation should be retried. +/// +/// {@category attachments} +typedef AttachmentExceptionHandler = Future Function( + Attachment attachment, + Object exception, + StackTrace stackTrace, +); + +/// Interface for handling errors during attachment operations. +/// Implementations determine whether failed operations should be retried. +/// Attachment records are archived if an operation fails and should not be retried. +/// +/// {@category attachments} +@experimental +abstract interface class AttachmentErrorHandler { + /// Creates an implementation of an error handler by delegating to the + /// individual functions for delete, download and upload errors. + const factory AttachmentErrorHandler({ + required AttachmentExceptionHandler onDeleteError, + required AttachmentExceptionHandler onDownloadError, + required AttachmentExceptionHandler onUploadError, + }) = _FunctionBasedErrorHandler; + + /// Determines whether the provided attachment download operation should be retried. + /// + /// [attachment] The attachment involved in the failed download operation. + /// [exception] The exception that caused the download failure. + /// [stackTrace] The [StackTrace] when the exception was caught. + /// + /// Returns `true` if the download operation should be retried, `false` otherwise. + Future onDownloadError( + Attachment attachment, + Object exception, + StackTrace stackTrace, + ); + + /// Determines whether the provided attachment upload operation should be retried. + /// + /// [attachment] The attachment involved in the failed upload operation. + /// [exception] The exception that caused the upload failure. + /// [stackTrace] The [StackTrace] when the exception was caught. + /// + /// Returns `true` if the upload operation should be retried, `false` otherwise. + Future onUploadError( + Attachment attachment, + Object exception, + StackTrace stackTrace, + ); + + /// Determines whether the provided attachment delete operation should be retried. + /// + /// [attachment] The attachment involved in the failed delete operation. + /// [exception] The exception that caused the delete failure. + /// [stackTrace] The [StackTrace] when the exception was caught. + /// + /// Returns `true` if the delete operation should be retried, `false` otherwise. + Future onDeleteError( + Attachment attachment, + Object exception, + StackTrace stackTrace, + ); +} + +final class _FunctionBasedErrorHandler implements AttachmentErrorHandler { + final AttachmentExceptionHandler _onDeleteError; + final AttachmentExceptionHandler _onDownloadError; + final AttachmentExceptionHandler _onUploadError; + + const _FunctionBasedErrorHandler( + {required AttachmentExceptionHandler onDeleteError, + required AttachmentExceptionHandler onDownloadError, + required AttachmentExceptionHandler onUploadError}) + : _onDeleteError = onDeleteError, + _onDownloadError = onDownloadError, + _onUploadError = onUploadError; + + @override + Future onDeleteError( + Attachment attachment, Object exception, StackTrace stackTrace) { + return _onDeleteError(attachment, exception, stackTrace); + } + + @override + Future onDownloadError( + Attachment attachment, Object exception, StackTrace stackTrace) { + return _onDownloadError(attachment, exception, stackTrace); + } + + @override + Future onUploadError( + Attachment attachment, Object exception, StackTrace stackTrace) { + return _onUploadError(attachment, exception, stackTrace); + } +} diff --git a/packages/powersync/lib/src/connector.dart b/packages/powersync_core/lib/src/connector.dart similarity index 91% rename from packages/powersync/lib/src/connector.dart rename to packages/powersync_core/lib/src/connector.dart index f8d58460..acb7b961 100644 --- a/packages/powersync/lib/src/connector.dart +++ b/packages/powersync_core/lib/src/connector.dart @@ -90,13 +90,13 @@ class PowerSyncCredentials { } factory PowerSyncCredentials.fromJson(Map parsed) { - String token = parsed['token']; + String token = parsed['token'] as String; DateTime? expiresAt = getExpiryDate(token); return PowerSyncCredentials( - endpoint: parsed['endpoint'], - token: parsed['token'], - userId: parsed['user_id'], + endpoint: parsed['endpoint'] as String, + token: token, + userId: parsed['user_id'] as String?, expiresAt: expiresAt); } @@ -110,9 +110,9 @@ class PowerSyncCredentials { // dart:convert doesn't like missing padding final rawData = base64Url.decode(base64.normalize(parts[1])); final text = Utf8Decoder().convert(rawData); - Map payload = jsonDecode(text); - if (payload.containsKey('exp') && payload['exp'] is int) { - return DateTime.fromMillisecondsSinceEpoch(payload['exp'] * 1000); + final payload = jsonDecode(text) as Map; + if (payload['exp'] case int exp) { + return DateTime.fromMillisecondsSinceEpoch(exp * 1000); } } return null; @@ -131,7 +131,7 @@ class PowerSyncCredentials { return Uri.parse(endpoint).resolve(path); } - _validateEndpoint() { + void _validateEndpoint() { final parsed = Uri.parse(endpoint); if ((!parsed.isScheme('http') && !parsed.isScheme('https')) || parsed.host.isEmpty) { @@ -162,14 +162,14 @@ class DevCredentials { factory DevCredentials.fromJson(Map parsed) { return DevCredentials( - endpoint: parsed['endpoint'], - token: parsed['token'], - userId: parsed['user_id']); + endpoint: parsed['endpoint'] as String, + token: parsed['token'] as String?, + userId: parsed['user_id'] as String?); } factory DevCredentials.fromString(String credentials) { var parsed = jsonDecode(credentials); - return DevCredentials.fromJson(parsed); + return DevCredentials.fromJson(parsed as Map); } static DevCredentials? fromOptionalString(String? credentials) { @@ -255,10 +255,12 @@ class DevConnector extends PowerSyncBackendConnector { if (res.statusCode == 200) { var parsed = jsonDecode(res.body); + var data = parsed['data'] as Map; + storeDevCredentials(DevCredentials( endpoint: endpoint, - token: parsed['data']['token'], - userId: parsed['data']['user_id'])); + token: data['token'] as String?, + userId: data['user_id'] as String?)); } else { throw http.ClientException(res.reasonPhrase ?? 'Request failed', uri); } @@ -281,7 +283,8 @@ class DevConnector extends PowerSyncBackendConnector { throw http.ClientException(res.reasonPhrase ?? 'Request failed', uri); } - return PowerSyncCredentials.fromJson(jsonDecode(res.body)['data']); + return PowerSyncCredentials.fromJson( + jsonDecode(res.body)['data'] as Map); } /// Upload changes using the PowerSync dev API. @@ -319,7 +322,7 @@ class DevConnector extends PowerSyncBackendConnector { final body = jsonDecode(response.body); // writeCheckpoint is optional, but reduces latency between writing, // and reading back the same change. - final String? writeCheckpoint = body['data']['write_checkpoint']; + final writeCheckpoint = body['data']['write_checkpoint'] as String?; await batch.complete(writeCheckpoint: writeCheckpoint); } } diff --git a/packages/powersync/lib/src/crud.dart b/packages/powersync_core/lib/src/crud.dart similarity index 71% rename from packages/powersync/lib/src/crud.dart rename to packages/powersync_core/lib/src/crud.dart index 3ca4c288..68ceab26 100644 --- a/packages/powersync/lib/src/crud.dart +++ b/packages/powersync_core/lib/src/crud.dart @@ -1,7 +1,9 @@ import 'dart:convert'; import 'package:collection/collection.dart'; -import 'package:powersync/sqlite3_common.dart' as sqlite; +import 'package:powersync_core/sqlite3_common.dart' as sqlite; + +import 'schema.dart'; /// A batch of client-side changes. class CrudBatch { @@ -68,6 +70,14 @@ class CrudEntry { /// ID of the changed row. final String id; + /// An optional metadata string attached to this entry at the time the write + /// has been issued. + /// + /// For tables where [Table.trackMetadata] is enabled, a hidden `_metadata` + /// column is added to this table that can be used during updates to attach + /// a hint to the update thas is preserved here. + final String? metadata; + /// Data associated with the change. /// /// For PUT, this is contains all non-null columns of the row. @@ -77,13 +87,35 @@ class CrudEntry { /// For DELETE, this is null. final Map? opData; - CrudEntry(this.clientId, this.op, this.table, this.id, this.transactionId, - this.opData); + /// Old values before an update. + /// + /// This is only tracked for tables for which this has been enabled by setting + /// the [Table.trackPreviousValues]. + final Map? previousValues; + + CrudEntry( + this.clientId, + this.op, + this.table, + this.id, + this.transactionId, + this.opData, { + this.previousValues, + this.metadata, + }); factory CrudEntry.fromRow(sqlite.Row row) { - final data = jsonDecode(row['data']); - return CrudEntry(row['id'], UpdateType.fromJsonChecked(data['op'])!, - data['type'], data['id'], row['tx_id'], data['data']); + final data = jsonDecode(row['data'] as String); + return CrudEntry( + row['id'] as int, + UpdateType.fromJsonChecked(data['op'] as String)!, + data['type'] as String, + data['id'] as String, + row['tx_id'] as int, + data['data'] as Map?, + previousValues: data['old'] as Map?, + metadata: data['metadata'] as String?, + ); } /// Converts the change to JSON format, as required by the dev crud API. @@ -94,7 +126,9 @@ class CrudEntry { 'type': table, 'id': id, 'tx_id': transactionId, - 'data': opData + 'data': opData, + 'metadata': metadata, + 'old': previousValues, }; } @@ -111,13 +145,13 @@ class CrudEntry { other.op == op && other.table == table && other.id == id && - const MapEquality().equals(other.opData, opData)); + const MapEquality().equals(other.opData, opData)); } @override int get hashCode { return Object.hash(transactionId, clientId, op.toJson(), table, id, - const MapEquality().hash(opData)); + const MapEquality().hash(opData)); } } diff --git a/packages/powersync_core/lib/src/database/active_instances.dart b/packages/powersync_core/lib/src/database/active_instances.dart new file mode 100644 index 00000000..d93ac4a3 --- /dev/null +++ b/packages/powersync_core/lib/src/database/active_instances.dart @@ -0,0 +1,49 @@ +import 'package:meta/meta.dart'; +import 'package:sqlite_async/sqlite_async.dart'; + +/// A collection of PowerSync database instances that are using the same +/// underlying SQLite database. +/// +/// We expect that each group will only ever have one database because we +/// encourage users to manage their databases as singletons. So, we print a +/// warning when two databases are part of the same group. +/// +/// This can only detect two database instances being opened on the same +/// isolate, we can't provide these checks acros isolates. Since most users +/// aren't opening databases on background isolates though, this still guards +/// against most misuses. +@internal +final class ActiveDatabaseGroup { + int refCount = 0; + + /// Use to prevent multiple connections from being opened concurrently + final Mutex syncConnectMutex = Mutex(); + final Mutex syncMutex; + final Mutex crudMutex; + + final String identifier; + + ActiveDatabaseGroup._(this.identifier) + : syncMutex = Mutex(identifier: '$identifier-sync'), + crudMutex = Mutex(identifier: '$identifier-crud'); + + Future close() async { + if (--refCount == 0) { + final removedGroup = _activeGroups.remove(identifier); + assert(removedGroup == this); + + await syncConnectMutex.close(); + await syncMutex.close(); + await crudMutex.close(); + } + } + + static final Map _activeGroups = {}; + + static ActiveDatabaseGroup referenceDatabase(String identifier) { + final group = _activeGroups.putIfAbsent( + identifier, () => ActiveDatabaseGroup._(identifier)); + group.refCount++; + return group; + } +} diff --git a/packages/powersync_core/lib/src/database/core_version.dart b/packages/powersync_core/lib/src/database/core_version.dart new file mode 100644 index 00000000..1a3ca3da --- /dev/null +++ b/packages/powersync_core/lib/src/database/core_version.dart @@ -0,0 +1,71 @@ +import 'package:sqlite_async/sqlite3_common.dart'; + +/// A parsed (major, minor, patch) version triple representing a version of the +/// loaded core extension. +extension type const PowerSyncCoreVersion((int, int, int) _tuple) { + int get major => _tuple.$1; + int get minor => _tuple.$2; + int get patch => _tuple.$3; + + int compareTo(PowerSyncCoreVersion other) { + return switch (major.compareTo(other.major)) { + 0 => switch (minor.compareTo(other.minor)) { + 0 => patch.compareTo(other.patch), + var other => other, + }, + var other => other, + }; + } + + bool operator <(PowerSyncCoreVersion other) => compareTo(other) < 0; + bool operator >=(PowerSyncCoreVersion other) => compareTo(other) >= 0; + + String get versionString => '$major.$minor.$patch'; + + void checkSupported() { + const isWeb = bool.fromEnvironment('dart.library.js_interop'); + + if (this < minimum || this >= maximumExclusive) { + var message = + 'Unsupported powersync extension version. This version of the ' + 'PowerSync SDK needs >=${minimum.versionString} ' + '<${maximumExclusive.versionString}, ' + 'but detected version $versionString.'; + if (isWeb) { + message += + '\nTry downloading the updated assets: https://docs.powersync.com/client-sdk-references/flutter/flutter-web-support#assets'; + } + + throw SqliteException(1, message); + } + } + + /// Parses the output of `powersync_rs_version()`, e.g. `0.3.9/5d64f366`, into + /// a [PowerSyncCoreVersion]. + static PowerSyncCoreVersion parse(String version) { + try { + final [major, minor, patch] = + version.split(RegExp(r'[./]')).take(3).map(int.parse).toList(); + + return PowerSyncCoreVersion((major, minor, patch)); + } catch (e) { + throw SqliteException(1, + 'Unsupported powersync extension version. Need >=0.2.0 <1.0.0, got: $version. Details: $e'); + } + } + + /// The minimum version of the sqlite core extensions we support. We check + /// this version when opening databases to fail early and with an actionable + /// error message. + // Note: When updating this, also update: + // + // - scripts/init_powersync_core_binary.dart + // - scripts/download_core_binary_demos.dart + // - packages/sqlite3_wasm_build/build.sh + // - Android and Darwin (CocoaPods and SwiftPM) in powersync_flutter_libs + static const minimum = PowerSyncCoreVersion((0, 4, 6)); + + /// The first version of the core extensions that this version of the Dart + /// SDK doesn't support. + static const maximumExclusive = PowerSyncCoreVersion((1, 0, 0)); +} diff --git a/packages/powersync/lib/src/database/native/native_powersync_database.dart b/packages/powersync_core/lib/src/database/native/native_powersync_database.dart similarity index 53% rename from packages/powersync/lib/src/database/native/native_powersync_database.dart rename to packages/powersync_core/lib/src/database/native/native_powersync_database.dart index e62f8ac8..40489365 100644 --- a/packages/powersync/lib/src/database/native/native_powersync_database.dart +++ b/packages/powersync_core/lib/src/database/native/native_powersync_database.dart @@ -1,25 +1,29 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:isolate'; import 'package:meta/meta.dart'; import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; -import 'package:powersync/src/abort_controller.dart'; -import 'package:powersync/src/bucket_storage.dart'; -import 'package:powersync/src/connector.dart'; -import 'package:powersync/src/database/powersync_database.dart'; -import 'package:powersync/src/database/powersync_db_mixin.dart'; -import 'package:powersync/src/isolate_completer.dart'; -import 'package:powersync/src/log.dart'; -import 'package:powersync/src/log_internal.dart'; -import 'package:powersync/src/open_factory/abstract_powersync_open_factory.dart'; -import 'package:powersync/src/open_factory/native/native_open_factory.dart'; -import 'package:powersync/src/schema.dart'; -import 'package:powersync/src/schema_logic.dart'; -import 'package:powersync/src/streaming_sync.dart'; -import 'package:powersync/src/sync_status.dart'; +import 'package:powersync_core/src/abort_controller.dart'; +import 'package:powersync_core/src/sync/bucket_storage.dart'; +import 'package:powersync_core/src/connector.dart'; +import 'package:powersync_core/src/database/powersync_database.dart'; +import 'package:powersync_core/src/database/powersync_db_mixin.dart'; +import 'package:powersync_core/src/isolate_completer.dart'; +import 'package:powersync_core/src/log.dart'; +import 'package:powersync_core/src/log_internal.dart'; +import 'package:powersync_core/src/open_factory/abstract_powersync_open_factory.dart'; +import 'package:powersync_core/src/open_factory/native/native_open_factory.dart'; +import 'package:powersync_core/src/schema.dart'; +import 'package:powersync_core/src/sync/internal_connector.dart'; +import 'package:powersync_core/src/sync/options.dart'; +import 'package:powersync_core/src/sync/streaming_sync.dart'; +import 'package:powersync_core/src/sync/sync_status.dart'; import 'package:sqlite_async/sqlite3_common.dart'; import 'package:sqlite_async/sqlite_async.dart'; +// ignore: implementation_imports +import 'package:sqlite_async/src/native/native_isolate_mutex.dart'; /// A PowerSync managed database. /// @@ -80,8 +84,12 @@ class PowerSyncDatabaseImpl DefaultSqliteOpenFactory factory = // ignore: deprecated_member_use_from_same_package PowerSyncOpenFactory(path: path, sqliteSetup: sqliteSetup); - return PowerSyncDatabaseImpl.withFactory(factory, - schema: schema, maxReaders: maxReaders, logger: logger); + return PowerSyncDatabaseImpl.withFactory( + factory, + schema: schema, + maxReaders: maxReaders, + logger: logger, + ); } /// Open a [PowerSyncDatabase] with a [PowerSyncOpenFactory]. @@ -93,13 +101,17 @@ class PowerSyncDatabaseImpl /// /// [logger] defaults to [autoLogger], which logs to the console in debug builds. factory PowerSyncDatabaseImpl.withFactory( - DefaultSqliteOpenFactory openFactory, - {required Schema schema, - int maxReaders = SqliteDatabase.defaultMaxReaders, - Logger? logger}) { + DefaultSqliteOpenFactory openFactory, { + required Schema schema, + int maxReaders = SqliteDatabase.defaultMaxReaders, + Logger? logger, + }) { final db = SqliteDatabase.withFactory(openFactory, maxReaders: maxReaders); return PowerSyncDatabaseImpl.withDatabase( - schema: schema, database: db, logger: logger); + schema: schema, + database: db, + logger: logger, + ); } /// Open a PowerSyncDatabase on an existing [SqliteDatabase]. @@ -107,89 +119,116 @@ class PowerSyncDatabaseImpl /// Migrations are run on the database when this constructor is called. /// /// [logger] defaults to [autoLogger], which logs to the console in debug builds.s - PowerSyncDatabaseImpl.withDatabase( - {required this.schema, required this.database, Logger? logger}) { - if (logger != null) { - this.logger = logger; - } else { - this.logger = autoLogger; - } + PowerSyncDatabaseImpl.withDatabase({ + required this.schema, + required this.database, + Logger? logger, + }) { + this.logger = logger ?? autoLogger; isInitialized = baseInit(); } @override @internal + Future connectInternal({ + required PowerSyncBackendConnector connector, + required ResolvedSyncOptions options, + required List initiallyActiveStreams, + required Stream> activeStreams, + required AbortController abort, + required Zone asyncWorkZone, + }) async { + final dbRef = database.isolateConnectionFactory(); - /// Connect to the PowerSync service, and keep the databases in sync. - /// - /// The connection is automatically re-opened if it fails for any reason. - /// - /// Status changes are reported on [statusStream]. - baseConnect( - {required PowerSyncBackendConnector connector, - - /// Throttle time between CRUD operations - /// Defaults to 10 milliseconds. - required Duration crudThrottleTime, - required Future Function() reconnect, - Map? params}) async { - await initialize(); - - // Disconnect if connected - await disconnect(); - final disconnector = AbortController(); - disconnecter = disconnector; + bool triedSpawningIsolate = false; + StreamSubscription? crudUpdateSubscription; + StreamSubscription? activeStreamsSubscription; + final receiveMessages = ReceivePort(); + final receiveUnhandledErrors = ReceivePort(); + final receiveExit = ReceivePort(); + + SendPort? initPort; + final hasInitPort = Completer(); + final receivedIsolateExit = Completer(); + + Future waitForShutdown() async { + // Only complete the abortion signal after the isolate shuts down. This + // ensures absolutely no trace of this sync iteration remains. + if (triedSpawningIsolate) { + await receivedIsolateExit.future; + } - await isInitialized; - final dbRef = database.isolateConnectionFactory(); - ReceivePort rPort = ReceivePort(); - StreamSubscription? crudUpdateSubscription; - rPort.listen((data) async { + // Cleanup + crudUpdateSubscription?.cancel(); + activeStreamsSubscription?.cancel(); + receiveMessages.close(); + receiveUnhandledErrors.close(); + receiveExit.close(); + + // Clear status apart from lastSyncedAt + setStatus(SyncStatus(lastSyncedAt: currentStatus.lastSyncedAt)); + abort.completeAbort(); + } + + Future close() async { + initPort?.send(['close']); + await waitForShutdown(); + } + + Future handleMessage(Object? data) async { if (data is List) { - String action = data[0]; - if (action == "getCredentials") { + String action = data[0] as String; + if (action == "getCredentialsCached") { await (data[1] as PortCompleter).handle(() async { final token = await connector.getCredentialsCached(); logger.fine('Credentials: $token'); return token; }); - } else if (action == "invalidateCredentials") { + } else if (action == "prefetchCredentials") { logger.fine('Refreshing credentials'); + final invalidate = data[2] as bool; + await (data[1] as PortCompleter).handle(() async { - await connector.prefetchCredentials(); + if (invalidate) { + connector.invalidateCredentials(); + } + return await connector.prefetchCredentials(); }); } else if (action == 'init') { - SendPort port = data[1]; - var crudStream = - database.onChange(['ps_crud'], throttle: crudThrottleTime); + final port = initPort = data[1] as SendPort; + hasInitPort.complete(); + var crudStream = database + .onChange(['ps_crud'], throttle: options.crudThrottleTime); crudUpdateSubscription = crudStream.listen((event) { port.send(['update']); }); - disconnector.onAbort.then((_) { - port.send(['close']); - }).ignore(); + + activeStreamsSubscription = activeStreams.listen((streams) { + port.send(['changed_subscriptions', streams]); + }); } else if (action == 'uploadCrud') { await (data[1] as PortCompleter).handle(() async { await connector.uploadData(this); }); } else if (action == 'status') { - final SyncStatus status = data[1]; + final SyncStatus status = data[1] as SyncStatus; setStatus(status); - } else if (action == 'close') { - // Clear status apart from lastSyncedAt - setStatus(SyncStatus(lastSyncedAt: currentStatus.lastSyncedAt)); - rPort.close(); - crudUpdateSubscription?.cancel(); } else if (action == 'log') { - LogRecord record = data[1]; + LogRecord record = data[1] as LogRecord; logger.log( record.level, record.message, record.error, record.stackTrace); } } - }); + } + + // This function is called in a Zone marking the connection lock as locked. + // This is used to prevent reentrant calls to the lock (which would be a + // deadlock). However, the lock is returned as soon as this function + // returns - and handleMessage may run later. So, make sure we run those + // callbacks in the parent zone. + receiveMessages.listen(asyncWorkZone.bindUnaryCallback(handleMessage)); - var errorPort = ReceivePort(); - errorPort.listen((message) async { + receiveUnhandledErrors.listen((message) async { // Sample error: // flutter: [PowerSync] WARNING: 2023-06-28 16:34:11.566122: Sync Isolate error // flutter: [Connection closed while receiving data, #0 IOClient.send. (package:http/src/io_client.dart:76:13) @@ -200,38 +239,47 @@ class PowerSyncDatabaseImpl // ... logger.severe('Sync Isolate error', message); - // Reconnect - // Use the param like this instead of directly calling connect(), to avoid recursive - // locks in some edge cases. - reconnect(); + // Fatal errors are enabled, so the isolate will exit soon, causing us to + // complete the abort controller which will make the db mixin reconnect if + // necessary. There's no need to reconnect manually. }); - disconnected() { - disconnector.completeAbort(); - disconnecter = null; - rPort.close(); - // Clear status apart from lastSyncedAt - setStatus(SyncStatus(lastSyncedAt: currentStatus.lastSyncedAt)); + // Don't spawn isolate if this operation was cancelled already. + if (abort.aborted) { + return waitForShutdown(); } - var exitPort = ReceivePort(); - exitPort.listen((message) { + receiveExit.listen((message) { logger.fine('Sync Isolate exit'); - disconnected(); + receivedIsolateExit.complete(); }); - if (disconnecter?.aborted == true) { - disconnected(); - return; - } - - Isolate.spawn( - _powerSyncDatabaseIsolate, - _PowerSyncDatabaseIsolateArgs( - rPort.sendPort, dbRef, retryDelay, clientParams), - debugName: 'PowerSyncDatabase', - onError: errorPort.sendPort, - onExit: exitPort.sendPort); + final crudMutex = group.crudMutex as SimpleMutex; + final syncMutex = group.syncMutex as SimpleMutex; + + // Spawning the isolate can't be interrupted + triedSpawningIsolate = true; + await Isolate.spawn( + _syncIsolate, + _PowerSyncDatabaseIsolateArgs( + receiveMessages.sendPort, + dbRef, + options, + crudMutex.shared, + syncMutex.shared, + jsonEncode(schema), + ), + debugName: 'Sync ${database.openFactory.path}', + onError: receiveUnhandledErrors.sendPort, + errorsAreFatal: true, + onExit: receiveExit.sendPort, + ); + await hasInitPort.future; + + abort.onAbort.whenComplete(close); + + // Automatically complete the abort controller once the isolate exits. + unawaited(waitForShutdown()); } /// Takes a read lock, without starting a transaction. @@ -255,61 +303,84 @@ class PowerSyncDatabaseImpl return database.writeLock(callback, debugContext: debugContext, lockTimeout: lockTimeout); } - - @override - Future updateSchema(Schema schema) { - if (disconnecter != null) { - throw AssertionError('Cannot update schema while connected'); - } - schema.validate(); - this.schema = schema; - return updateSchemaInIsolate(database, schema); - } } class _PowerSyncDatabaseIsolateArgs { final SendPort sPort; final IsolateConnectionFactory dbRef; - final Duration retryDelay; - final Map? parameters; + final ResolvedSyncOptions options; + final SerializedMutex crudMutex; + final SerializedMutex syncMutex; + final String schemaJson; _PowerSyncDatabaseIsolateArgs( - this.sPort, this.dbRef, this.retryDelay, this.parameters); + this.sPort, + this.dbRef, + this.options, + this.crudMutex, + this.syncMutex, + this.schemaJson, + ); } -Future _powerSyncDatabaseIsolate( - _PowerSyncDatabaseIsolateArgs args) async { +Future _syncIsolate(_PowerSyncDatabaseIsolateArgs args) async { final sPort = args.sPort; - ReceivePort rPort = ReceivePort(); + final rPort = ReceivePort(); StreamController crudUpdateController = StreamController.broadcast(); final upstreamDbClient = args.dbRef.upstreamPort.open(); + final crudMutex = args.crudMutex.open(); + final syncMutex = args.syncMutex.open(); + CommonDatabase? db; final Mutex mutex = args.dbRef.mutex.open(); StreamingSyncImplementation? openedStreamingSync; + StreamSubscription? localUpdatesSubscription; + + Completer shutdownCompleter = Completer(); + + Future shutdown() { + if (!shutdownCompleter.isCompleted) { + shutdownCompleter.complete(Future(() async { + await openedStreamingSync?.abort(); + + localUpdatesSubscription?.cancel(); + db?.dispose(); + crudUpdateController.close(); + upstreamDbClient.close(); - rPort.listen((message) async { - if (message is List) { - String action = message[0]; - if (action == 'update') { - crudUpdateController.add('update'); - } else if (action == 'close') { // The SyncSqliteConnection uses this mutex // It needs to be closed before killing the isolate // in order to free the mutex for other operations. await mutex.close(); - db?.dispose(); - crudUpdateController.close(); - upstreamDbClient.close(); - // Abort any open http requests, and wait for it to be closed properly - await openedStreamingSync?.abort(); - // No kill the Isolate + await crudMutex.close(); + await syncMutex.close(); + rPort.close(); + + // TODO: If we closed our resources properly, this wouldn't be necessary... Isolate.current.kill(); + })); + } + + return shutdownCompleter.future; + } + + rPort.listen((message) async { + if (message is List) { + String action = message[0] as String; + if (action == 'update') { + if (!crudUpdateController.isClosed) { + crudUpdateController.add('update'); + } + } else if (action == 'close') { + await shutdown(); + } else if (action == 'changed_subscriptions') { + openedStreamingSync + ?.updateSubscriptions(message[1] as List); } } }); - Isolate.current.addOnExitListener(sPort, response: const ['close']); - sPort.send(["init", rPort.sendPort]); + sPort.send(['init', rPort.sendPort]); // Is there a way to avoid the overhead if logging is not enabled? // This only takes effect in this isolate. @@ -317,24 +388,25 @@ Future _powerSyncDatabaseIsolate( isolateLogger.onRecord.listen((record) { var copy = LogRecord(record.level, record.message, record.loggerName, record.error, record.stackTrace); - sPort.send(["log", copy]); + sPort.send(['log', copy]); }); - Future loadCredentials() async { + Future getCredentialsCached() async { final r = IsolateResult(); - sPort.send(["getCredentials", r.completer]); + sPort.send(['getCredentialsCached', r.completer]); return r.future; } - Future invalidateCredentials() async { - final r = IsolateResult(); - sPort.send(["invalidateCredentials", r.completer]); + Future prefetchCredentials( + {required bool invalidate}) async { + final r = IsolateResult(); + sPort.send(['prefetchCredentials', r.completer, invalidate]); return r.future; } Future uploadCrud() async { final r = IsolateResult(); - sPort.send(["uploadCrud", r.completer]); + sPort.send(['uploadCrud', r.completer]); return r.future; } @@ -345,14 +417,19 @@ Future _powerSyncDatabaseIsolate( final storage = BucketStorage(connection); final sync = StreamingSyncImplementation( - adapter: storage, - credentialsCallback: loadCredentials, - invalidCredentialsCallback: invalidateCredentials, + adapter: storage, + schemaJson: args.schemaJson, + connector: InternalConnector( + getCredentialsCached: getCredentialsCached, + prefetchCredentials: prefetchCredentials, uploadCrud: uploadCrud, - crudUpdateTriggerStream: crudUpdateController.stream, - retryDelay: args.retryDelay, - client: http.Client(), - syncParameters: args.parameters); + ), + crudUpdateTriggerStream: crudUpdateController.stream, + options: args.options, + client: http.Client(), + crudMutex: crudMutex, + syncMutex: syncMutex, + ); openedStreamingSync = sync; sync.streamingSync(); sync.statusStream.listen((event) { @@ -372,18 +449,18 @@ Future _powerSyncDatabaseIsolate( } } - db!.updates.listen((event) { + localUpdatesSubscription = db!.updatesSync.listen((event) { updatedTables.add(event.tableName); updateDebouncer ??= Timer(const Duration(milliseconds: 1), maybeFireUpdates); }); - }, (error, stack) { + }, (error, stack) async { // Properly dispose the database if an uncaught error occurs. // Unfortunately, this does not handle disposing while the database is opening. // This should be rare - any uncaught error is a bug. And in most cases, // it should occur after the database is already open. - db?.dispose(); - throw error; + await shutdown(); + Error.throwWithStackTrace(error, stack); }); } diff --git a/packages/powersync/lib/src/database/powersync_database.dart b/packages/powersync_core/lib/src/database/powersync_database.dart similarity index 62% rename from packages/powersync/lib/src/database/powersync_database.dart rename to packages/powersync_core/lib/src/database/powersync_database.dart index 96a48ef6..4de7ea92 100644 --- a/packages/powersync/lib/src/database/powersync_database.dart +++ b/packages/powersync_core/lib/src/database/powersync_database.dart @@ -1,7 +1,7 @@ import 'package:logging/logging.dart'; -import 'package:powersync/src/database/powersync_database_impl.dart'; -import 'package:powersync/src/database/powersync_db_mixin.dart'; -import 'package:powersync/src/open_factory/abstract_powersync_open_factory.dart'; +import 'package:powersync_core/src/database/powersync_database_impl.dart'; +import 'package:powersync_core/src/database/powersync_db_mixin.dart'; +import 'package:powersync_core/src/open_factory/abstract_powersync_open_factory.dart'; import 'package:sqlite_async/sqlite_async.dart'; import '../schema.dart'; @@ -32,19 +32,21 @@ abstract class PowerSyncDatabase /// A maximum of [maxReaders] concurrent read transactions are allowed. /// /// [logger] defaults to [autoLogger], which logs to the console in debug builds. - factory PowerSyncDatabase( - {required Schema schema, - required String path, - Logger? logger, - @Deprecated("Use [PowerSyncDatabase.withFactory] instead.") - // ignore: deprecated_member_use_from_same_package - SqliteConnectionSetup? sqliteSetup}) { + factory PowerSyncDatabase({ + required Schema schema, + required String path, + Logger? logger, + @Deprecated("Use [PowerSyncDatabase.withFactory] instead.") + // ignore: deprecated_member_use_from_same_package + SqliteConnectionSetup? sqliteSetup, + }) { return PowerSyncDatabaseImpl( - schema: schema, - path: path, - logger: logger, - // ignore: deprecated_member_use_from_same_package - sqliteSetup: sqliteSetup); + schema: schema, + path: path, + logger: logger, + // ignore: deprecated_member_use_from_same_package + sqliteSetup: sqliteSetup, + ); } /// Open a [PowerSyncDatabase] with a [PowerSyncOpenFactory]. @@ -55,12 +57,18 @@ abstract class PowerSyncDatabase /// Subclass [PowerSyncOpenFactory] to add custom logic to this process. /// /// [logger] defaults to [autoLogger], which logs to the console in debug builds. - factory PowerSyncDatabase.withFactory(DefaultSqliteOpenFactory openFactory, - {required Schema schema, - int maxReaders = SqliteDatabase.defaultMaxReaders, - Logger? logger}) { - return PowerSyncDatabaseImpl.withFactory(openFactory, - schema: schema, maxReaders: maxReaders, logger: logger); + factory PowerSyncDatabase.withFactory( + DefaultSqliteOpenFactory openFactory, { + required Schema schema, + int maxReaders = SqliteDatabase.defaultMaxReaders, + Logger? logger, + }) { + return PowerSyncDatabaseImpl.withFactory( + openFactory, + schema: schema, + maxReaders: maxReaders, + logger: logger, + ); } /// Open a PowerSyncDatabase on an existing [SqliteDatabase]. @@ -68,11 +76,16 @@ abstract class PowerSyncDatabase /// Migrations are run on the database when this constructor is called. /// /// [logger] defaults to [autoLogger], which logs to the console in debug builds. - factory PowerSyncDatabase.withDatabase( - {required Schema schema, - required SqliteDatabase database, - Logger? loggers}) { + factory PowerSyncDatabase.withDatabase({ + required Schema schema, + required SqliteDatabase database, + Logger? logger, + @Deprecated("Use [logger] instead") Logger? loggers, + }) { return PowerSyncDatabaseImpl.withDatabase( - schema: schema, database: database); + schema: schema, + database: database, + logger: loggers ?? logger, + ); } } diff --git a/packages/powersync/lib/src/database/powersync_database_impl.dart b/packages/powersync_core/lib/src/database/powersync_database_impl.dart similarity index 87% rename from packages/powersync/lib/src/database/powersync_database_impl.dart rename to packages/powersync_core/lib/src/database/powersync_database_impl.dart index 7c98975f..7a439ef0 100644 --- a/packages/powersync/lib/src/database/powersync_database_impl.dart +++ b/packages/powersync_core/lib/src/database/powersync_database_impl.dart @@ -6,4 +6,4 @@ export 'powersync_database_impl_stub.dart' // ignore: uri_does_not_exist if (dart.library.io) './native/native_powersync_database.dart' // ignore: uri_does_not_exist - if (dart.library.html) './web/web_powersync_database.dart'; + if (dart.library.js_interop) './web/web_powersync_database.dart'; diff --git a/packages/powersync/lib/src/database/powersync_database_impl_stub.dart b/packages/powersync_core/lib/src/database/powersync_database_impl_stub.dart similarity index 79% rename from packages/powersync/lib/src/database/powersync_database_impl_stub.dart rename to packages/powersync_core/lib/src/database/powersync_database_impl_stub.dart index 68faf631..ae891cb7 100644 --- a/packages/powersync/lib/src/database/powersync_database_impl_stub.dart +++ b/packages/powersync_core/lib/src/database/powersync_database_impl_stub.dart @@ -2,9 +2,12 @@ import 'dart:async'; import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; -import 'package:powersync/sqlite_async.dart'; -import 'package:powersync/src/database/powersync_db_mixin.dart'; -import 'package:powersync/src/open_factory/abstract_powersync_open_factory.dart'; +import 'package:powersync_core/sqlite_async.dart'; +import 'package:powersync_core/src/abort_controller.dart'; +import 'package:powersync_core/src/database/powersync_db_mixin.dart'; +import 'package:powersync_core/src/open_factory/abstract_powersync_open_factory.dart'; +import '../sync/options.dart'; +import '../sync/streaming_sync.dart'; import 'powersync_database.dart'; import '../connector.dart'; @@ -24,6 +27,9 @@ class PowerSyncDatabaseImpl @override Schema get schema => throw UnimplementedError(); + @override + set schema(Schema s) => throw UnimplementedError(); + @override SqliteDatabase get database => throw UnimplementedError(); @@ -77,10 +83,11 @@ class PowerSyncDatabaseImpl /// Migrations are run on the database when this constructor is called. /// /// [logger] defaults to [autoLogger], which logs to the console in debug builds.s - factory PowerSyncDatabaseImpl.withDatabase( - {required Schema schema, - required SqliteDatabase database, - Logger? loggers}) { + factory PowerSyncDatabaseImpl.withDatabase({ + required Schema schema, + required SqliteDatabase database, + Logger? logger, + }) { throw UnimplementedError(); } @@ -101,21 +108,19 @@ class PowerSyncDatabaseImpl throw UnimplementedError(); } - @override - Future updateSchema(Schema schema) { - throw UnimplementedError(); - } - @override Logger get logger => throw UnimplementedError(); @override @internal - Future baseConnect( - {required PowerSyncBackendConnector connector, - required Duration crudThrottleTime, - required Future Function() reconnect, - Map? params}) { + Future connectInternal({ + required PowerSyncBackendConnector connector, + required AbortController abort, + required List initiallyActiveStreams, + required Stream> activeStreams, + required Zone asyncWorkZone, + required ResolvedSyncOptions options, + }) { throw UnimplementedError(); } diff --git a/packages/powersync_core/lib/src/database/powersync_db_mixin.dart b/packages/powersync_core/lib/src/database/powersync_db_mixin.dart new file mode 100644 index 00000000..fd722a2a --- /dev/null +++ b/packages/powersync_core/lib/src/database/powersync_db_mixin.dart @@ -0,0 +1,522 @@ +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; +import 'package:powersync_core/sqlite3_common.dart'; +import 'package:powersync_core/sqlite_async.dart'; +import 'package:powersync_core/src/abort_controller.dart'; +import 'package:powersync_core/src/connector.dart'; +import 'package:powersync_core/src/crud.dart'; +import 'package:powersync_core/src/database/active_instances.dart'; +import 'package:powersync_core/src/database/core_version.dart'; +import 'package:powersync_core/src/powersync_update_notification.dart'; +import 'package:powersync_core/src/schema.dart'; +import 'package:powersync_core/src/schema_logic.dart'; +import 'package:powersync_core/src/schema_logic.dart' as schema_logic; +import 'package:powersync_core/src/sync/connection_manager.dart'; +import 'package:powersync_core/src/sync/options.dart'; +import 'package:powersync_core/src/sync/sync_status.dart'; + +import '../sync/stream.dart'; +import '../sync/streaming_sync.dart'; + +mixin PowerSyncDatabaseMixin implements SqliteConnection { + /// Schema used for the local database. + Schema get schema; + + @internal + set schema(Schema schema); + + /// The underlying database. + /// + /// For the most part, behavior is the same whether querying on the underlying + /// database, or on [PowerSyncDatabase]. The main difference is in update notifications: + /// the underlying database reports updates to the underlying tables, while + /// [PowerSyncDatabase] reports updates to the higher-level views. + SqliteDatabase get database; + + /// The Logger used by this [PowerSyncDatabase]. + /// + /// The default is [autoLogger], which logs to the console in debug builds. + /// Use [debugLogger] to always log to the console. + /// Use [attachedLogger] to propagate logs to [Logger.root] for custom logging. + Logger get logger; + + @Deprecated("This field is unused, pass params to connect() instead") + Map? clientParams; + + late final ConnectionManager _connections; + + /// Current connection status. + SyncStatus get currentStatus => _connections.currentStatus; + + /// Use this stream to subscribe to connection status updates. + Stream get statusStream => _connections.statusStream; + + late final ActiveDatabaseGroup _activeGroup; + + /// An [ActiveDatabaseGroup] sharing mutexes for the sync client. + /// + /// This is used to ensure that, even if two databases to the same file are + /// open concurrently, they won't both open a sync stream. Doing so would + /// waste resources. + @internal + ActiveDatabaseGroup get group => _activeGroup; + + @override + + /// Broadcast stream that is notified of any table updates. + /// + /// Unlike in [SqliteDatabase.updates], the tables reported here are the + /// higher-level views as defined in the [Schema], and exclude the low-level + /// PowerSync tables. + late final Stream updates; + + /// Delay between retrying failed requests. + /// Defaults to 5 seconds. + /// Only has an effect if changed before calling [connect]. + @Deprecated('Set option when calling connect() instead') + Duration retryDelay = const Duration(seconds: 5); + + @protected + Future get isInitialized; + + @protected + Future baseInit() async { + String identifier = 'memory'; + try { + identifier = database.openFactory.path; + } catch (ignore) { + // The in-memory database used in some tests doesn't have an open factory. + } + + _activeGroup = ActiveDatabaseGroup.referenceDatabase(identifier); + if (_activeGroup.refCount > 1) { + logger.warning( + 'Multiple instances for the same database have been detected. ' + 'This can cause unexpected results, please check your PowerSync client ' + 'instantiation logic if this is not intentional', + ); + } + _connections = ConnectionManager(this); + updates = powerSyncUpdateNotifications(database.updates); + + await database.initialize(); + await _checkVersion(); + await database.execute('SELECT powersync_init()'); + await updateSchema(schema); + await _connections.resolveOfflineSyncStatus(); + } + + /// Check that a supported version of the powersync extension is loaded. + Future _checkVersion() async { + // Get version + String version; + try { + final row = + await database.get('SELECT powersync_rs_version() as version'); + version = row['version'] as String; + } catch (e) { + throw SqliteException( + 1, 'The powersync extension is not loaded correctly. Details: $e'); + } + + PowerSyncCoreVersion.parse(version).checkSupported(); + } + + /// Wait for initialization to complete. + /// + /// While initializing is automatic, this helps to catch and report initialization errors. + Future initialize() { + return isInitialized; + } + + /// Returns a [Future] which will resolve once at least one full sync cycle + /// has completed (meaninng that the first consistent checkpoint has been + /// reached across all buckets). + /// + /// When [priority] is null (the default), this method waits for the first + /// full sync checkpoint to complete. When set to a [StreamPriority] however, + /// it completes once all buckets within that priority (as well as those in + /// higher priorities) have been synchronized at least once. + Future waitForFirstSync({StreamPriority? priority}) async { + bool matches(SyncStatus status) { + if (priority == null) { + return status.hasSynced == true; + } else { + return status.statusForPriority(priority).hasSynced == true; + } + } + + return _connections.firstStatusMatching(matches); + } + + @protected + @visibleForTesting + void setStatus(SyncStatus status) { + _connections.manuallyChangeSyncStatus(status); + } + + @override + bool get closed { + return database.closed; + } + + /// Close the database, releasing resources. + /// + /// Also [disconnect]s any active connection. + /// + /// Once close is called, this connection cannot be used again - a new one + /// must be constructed. + @override + Future close() async { + // Don't close in the middle of the initialization process. + await isInitialized; + // Disconnect any active sync connection. + await disconnect(); + + if (!database.closed) { + // Now we can close the database + await database.close(); + + // If there are paused subscriptions on the status stream, don't delay + // closing the database because of that. + _connections.close(); + await _activeGroup.close(); + } + } + + /// Connect to the PowerSync service, and keep the databases in sync. + /// + /// The connection is automatically re-opened if it fails for any reason. + /// + /// To set sync parameters used in your sync rules (if any), use + /// [SyncOptions.params]. [SyncOptions] can also be used to tune the behavior + /// of the sync client, see that class for more information. + /// + /// Status changes are reported on [statusStream]. + Future connect({ + required PowerSyncBackendConnector connector, + SyncOptions? options, + @Deprecated('Use SyncOptions.crudThrottleTime instead') + Duration? crudThrottleTime, + Map? params, + }) async { + // The initialization process acquires a sync connect lock (through + // updateSchema), so ensure the database is ready before we try to acquire + // the lock for the connection. + await initialize(); + + final resolvedOptions = ResolvedSyncOptions.resolve( + options, + crudThrottleTime: crudThrottleTime, + // ignore: deprecated_member_use_from_same_package + retryDelay: retryDelay, + params: params, + ); + + await _connections.connect(connector: connector, options: resolvedOptions); + } + + /// Internal method to establish a sync client connection. + /// + /// This method will always be wrapped in an exclusive mutex through the + /// [connect] method and should not be called elsewhere. + /// This method will only be called internally when no other sync client is + /// active, so the method should not call [disconnect] itself. + @protected + @internal + Future connectInternal({ + required PowerSyncBackendConnector connector, + required ResolvedSyncOptions options, + required List initiallyActiveStreams, + required Stream> activeStreams, + required AbortController abort, + required Zone asyncWorkZone, + }); + + /// Close the sync connection. + /// + /// Use [connect] to connect again. + Future disconnect() async { + await _connections.disconnect(); + } + + /// Disconnect and clear the database. + /// + /// Use this when logging out. + /// + /// The database can still be queried after this is called, but the tables + /// would be empty. + /// + /// To preserve data in local-only tables, set [clearLocal] to false. + Future disconnectAndClear({bool clearLocal = true}) async { + await disconnect(); + + await writeTransaction((tx) async { + await tx.execute('select powersync_clear(?)', [clearLocal ? 1 : 0]); + }); + // The data has been deleted - reset these + setStatus(SyncStatus(lastSyncedAt: null, hasSynced: false)); + } + + @Deprecated('Use [disconnectAndClear] instead.') + Future disconnectedAndClear() async { + await disconnectAndClear(); + } + + /// Whether a connection to the PowerSync service is currently open. + bool get connected { + return currentStatus.connected; + } + + /// Replace the schema with a new version. + /// This is for advanced use cases - typically the schema should just be + /// specified once in the constructor. + /// + /// Cannot be used while connected - this should only be called before [connect]. + Future updateSchema(Schema schema) async { + schema.validate(); + + await _activeGroup.syncConnectMutex.lock(() async { + _connections.checkNotConnected(); + + this.schema = schema; + await database.writeLock((tx) => schema_logic.updateSchema(tx, schema)); + }); + } + + /// A connection factory that can be passed to different isolates. + /// + /// Use this to access the database in background isolates. + IsolateConnectionFactory isolateConnectionFactory() { + return database.isolateConnectionFactory(); + } + + /// Get an unique id for this client. + /// This id is only reset when the database is deleted. + Future getClientId() async { + final row = await get('SELECT powersync_client_id() as client_id'); + return row['client_id'] as String; + } + + /// Get upload queue size estimate and count. + Future getUploadQueueStats( + {bool includeSize = false}) async { + if (includeSize) { + final row = await getOptional( + 'SELECT SUM(cast(data as blob) + 20) as size, count(*) as count FROM ps_crud'); + return UploadQueueStats( + count: row?['count'] as int? ?? 0, size: row?['size'] as int? ?? 0); + } else { + final row = await getOptional('SELECT count(*) as count FROM ps_crud'); + return UploadQueueStats(count: row?['count'] as int? ?? 0); + } + } + + /// Get a batch of crud data to upload. + /// + /// Returns null if there is no data to upload. + /// + /// Use this from the [PowerSyncBackendConnector.uploadData]` callback. + /// + /// Once the data have been successfully uploaded, call [CrudBatch.complete] before + /// requesting the next batch. + /// + /// Use [limit] to specify the maximum number of updates to return in a single + /// batch. + /// + /// This method does include transaction ids in the result, but does not group + /// data by transaction. One batch may contain data from multiple transactions, + /// and a single transaction may be split over multiple batches. + Future getCrudBatch({int limit = 100}) async { + final rows = await getAll( + 'SELECT id, tx_id, data FROM ps_crud ORDER BY id ASC LIMIT ?', + [limit + 1]); + List all = [for (var row in rows) CrudEntry.fromRow(row)]; + + var haveMore = false; + if (all.length > limit) { + all.removeLast(); + haveMore = true; + } + if (all.isEmpty) { + return null; + } + final last = all[all.length - 1]; + return CrudBatch( + crud: all, + haveMore: haveMore, + complete: _crudCompletionCallback(last.clientId), + ); + } + + /// Get the next recorded transaction to upload. + /// + /// Returns null if there is no data to upload. + /// + /// Use this from the [PowerSyncBackendConnector.uploadData]` callback. + /// + /// Once the data have been successfully uploaded, call [CrudTransaction.complete] before + /// requesting the next transaction. + /// + /// Unlike [getCrudBatch], this only returns data from a single transaction at a time. + /// All data for the transaction is loaded into memory. + Future getNextCrudTransaction() { + return getCrudTransactions().firstOrNull; + } + + /// Returns a stream of completed transactions with local writes against the + /// database. + /// + /// This is typically used from the [PowerSyncBackendConnector.uploadData] + /// method. Each entry emitted by the stream is a full transaction containing + /// all local writes made while that transaction was active. + /// + /// Unlike [getNextCrudTransaction], which always returns the oldest + /// transaction that hasn't been [CrudTransaction.complete]d yet, this stream + /// can be used to receive multiple transactions. Calling + /// [CrudTransaction.complete] will mark that transaction and all prior + /// transactions emitted by the stream as completed. + /// + /// This can be used to upload multiple transactions in a single batch, e.g. + /// with: + /// + /// ```dart + /// CrudTransaction? lastTransaction; + /// final batch = []; + /// + /// await for (final transaction in powersync.nextCrudTransactions()) { + /// batch.addAll(transaction.crud); + /// lastTransaction = transaction; + /// + /// if (batch.length > 100) { + /// break; + /// } + /// } + /// + /// if (batch.isNotEmpty) { + /// await uploadBatch(batch); + /// lastTransaction!.complete(); + /// } + /// ``` + /// + /// If there is no local data to upload, the stream emits a single `onDone` + /// event. + Stream getCrudTransactions() async* { + var lastCrudItemId = -1; + const sql = ''' +WITH RECURSIVE crud_entries AS ( + SELECT id, tx_id, data FROM ps_crud WHERE id = (SELECT min(id) FROM ps_crud WHERE id > ?) + UNION ALL + SELECT ps_crud.id, ps_crud.tx_id, ps_crud.data FROM ps_crud + INNER JOIN crud_entries ON crud_entries.id + 1 = rowid + WHERE crud_entries.tx_id = ps_crud.tx_id +) +SELECT * FROM crud_entries; +'''; + + while (true) { + final nextTransaction = await getAll(sql, [lastCrudItemId]); + if (nextTransaction.isEmpty) { + break; + } + + final items = [for (var row in nextTransaction) CrudEntry.fromRow(row)]; + final last = items.last; + final txId = last.transactionId; + + yield CrudTransaction( + crud: items, + complete: _crudCompletionCallback(last.clientId), + transactionId: txId, + ); + lastCrudItemId = last.clientId; + } + } + + Future Function({String? writeCheckpoint}) _crudCompletionCallback( + int lastClientId) { + return ({String? writeCheckpoint}) async { + await writeTransaction((db) async { + await db.execute('DELETE FROM ps_crud WHERE id <= ?', [lastClientId]); + if (writeCheckpoint != null && + await db.getOptional('SELECT 1 FROM ps_crud LIMIT 1') == null) { + await db.execute( + 'UPDATE ps_buckets SET target_op = CAST(? as INTEGER) WHERE name=\'\$local\'', + [writeCheckpoint]); + } else { + await db.execute( + 'UPDATE ps_buckets SET target_op = $maxOpId WHERE name=\'\$local\''); + } + }); + }; + } + + /// Takes a read lock, without starting a transaction. + /// + /// In most cases, [readTransaction] should be used instead. + @override + Future readLock(Future Function(SqliteReadContext tx) callback, + {String? debugContext, Duration? lockTimeout}); + + /// Takes a global lock, without starting a transaction. + /// + /// In most cases, [writeTransaction] should be used instead. + @override + Future writeLock(Future Function(SqliteWriteContext tx) callback, + {String? debugContext, Duration? lockTimeout}); + + @override + Stream watch(String sql, + {List parameters = const [], + Duration throttle = const Duration(milliseconds: 30), + Iterable? triggerOnTables}) { + if (triggerOnTables == null || triggerOnTables.isEmpty) { + return database.watch(sql, parameters: parameters, throttle: throttle); + } + List powersyncTables = []; + for (String tableName in triggerOnTables) { + powersyncTables.add(tableName); + powersyncTables.add(_prefixTableNames(tableName, 'ps_data__')); + powersyncTables.add(_prefixTableNames(tableName, 'ps_data_local__')); + } + return database.watch(sql, + parameters: parameters, + throttle: throttle, + triggerOnTables: powersyncTables); + } + + @protected + String _prefixTableNames(String tableName, String prefix) { + String prefixedString = tableName.replaceRange(0, 0, prefix); + return prefixedString; + } + + @override + Future getAutoCommit() { + return database.getAutoCommit(); + } + + @override + Future refreshSchema() async { + await database.refreshSchema(); + } + + /// Create a [SyncStream] instance for the given [name] and [parameters]. + /// + /// Use [SyncStream.subscribe] to subscribe to the returned stream. + SyncStream syncStream(String name, [Map? parameters]) { + return _connections.syncStream(name, parameters); + } +} + +Stream powerSyncUpdateNotifications( + Stream inner) { + return inner + .map((update) => + PowerSyncUpdateNotification.fromUpdateNotification(update)) + .where((update) => update.isNotEmpty) + .cast(); +} diff --git a/packages/powersync/lib/src/database/web/web_powersync_database.dart b/packages/powersync_core/lib/src/database/web/web_powersync_database.dart similarity index 68% rename from packages/powersync/lib/src/database/web/web_powersync_database.dart rename to packages/powersync_core/lib/src/database/web/web_powersync_database.dart index f75c7beb..15a83c7d 100644 --- a/packages/powersync/lib/src/database/web/web_powersync_database.dart +++ b/packages/powersync_core/lib/src/database/web/web_powersync_database.dart @@ -1,20 +1,22 @@ import 'dart:async'; +import 'dart:convert'; import 'package:meta/meta.dart'; -import 'package:fetch_client/fetch_client.dart'; +import 'package:http/browser_client.dart'; import 'package:logging/logging.dart'; -import 'package:powersync/src/abort_controller.dart'; -import 'package:powersync/src/bucket_storage.dart'; -import 'package:powersync/src/connector.dart'; -import 'package:powersync/src/database/powersync_database.dart'; -import 'package:powersync/src/database/powersync_db_mixin.dart'; -import 'package:powersync/src/log.dart'; -import 'package:powersync/src/open_factory/abstract_powersync_open_factory.dart'; -import 'package:powersync/src/open_factory/web/web_open_factory.dart'; -import 'package:powersync/src/schema.dart'; -import 'package:powersync/src/streaming_sync.dart'; +import 'package:powersync_core/src/abort_controller.dart'; +import 'package:powersync_core/src/sync/bucket_storage.dart'; +import 'package:powersync_core/src/connector.dart'; +import 'package:powersync_core/src/database/powersync_database.dart'; +import 'package:powersync_core/src/database/powersync_db_mixin.dart'; +import 'package:powersync_core/src/log.dart'; +import 'package:powersync_core/src/open_factory/abstract_powersync_open_factory.dart'; +import 'package:powersync_core/src/open_factory/web/web_open_factory.dart'; +import 'package:powersync_core/src/schema.dart'; +import 'package:powersync_core/src/sync/internal_connector.dart'; +import 'package:powersync_core/src/sync/streaming_sync.dart'; import 'package:sqlite_async/sqlite_async.dart'; -import 'package:powersync/src/schema_logic.dart' as schema_logic; +import '../../sync/options.dart'; import '../../web/sync_controller.dart'; /// A PowerSync managed database. @@ -37,8 +39,6 @@ class PowerSyncDatabaseImpl @override SqliteDatabase database; - late final DefaultSqliteOpenFactory openFactory; - @override @protected late Future isInitialized; @@ -76,8 +76,12 @@ class PowerSyncDatabaseImpl SqliteConnectionSetup? sqliteSetup}) { // ignore: deprecated_member_use_from_same_package DefaultSqliteOpenFactory factory = PowerSyncOpenFactory(path: path); - return PowerSyncDatabaseImpl.withFactory(factory, - maxReaders: maxReaders, logger: logger, schema: schema); + return PowerSyncDatabaseImpl.withFactory( + factory, + maxReaders: maxReaders, + logger: logger, + schema: schema, + ); } /// Open a [PowerSyncDatabase] with a [PowerSyncOpenFactory]. @@ -95,8 +99,10 @@ class PowerSyncDatabaseImpl Logger? logger}) { final db = SqliteDatabase.withFactory(openFactory, maxReaders: 1); return PowerSyncDatabaseImpl.withDatabase( - schema: schema, logger: logger, database: db) - ..openFactory = openFactory; + schema: schema, + logger: logger, + database: db, + ); } /// Open a PowerSyncDatabase on an existing [SqliteDatabase]. @@ -104,8 +110,11 @@ class PowerSyncDatabaseImpl /// Migrations are run on the database when this constructor is called. /// /// [logger] defaults to [autoLogger], which logs to the console in debug builds. - PowerSyncDatabaseImpl.withDatabase( - {required this.schema, required this.database, Logger? logger}) { + PowerSyncDatabaseImpl.withDatabase({ + required this.schema, + required this.database, + Logger? logger, + }) { if (logger != null) { this.logger = logger; } else { @@ -116,58 +125,42 @@ class PowerSyncDatabaseImpl @override @internal - - /// Connect to the PowerSync service, and keep the databases in sync. - /// - /// The connection is automatically re-opened if it fails for any reason. - /// - /// Status changes are reported on [statusStream]. - baseConnect({ + Future connectInternal({ required PowerSyncBackendConnector connector, - - /// Throttle time between CRUD operations - /// Defaults to 10 milliseconds. - required Duration crudThrottleTime, - required Future Function() reconnect, - Map? params, + required AbortController abort, + required List initiallyActiveStreams, + required Stream> activeStreams, + required Zone asyncWorkZone, + required ResolvedSyncOptions options, }) async { - await initialize(); - - // Disconnect if connected - await disconnect(); - disconnecter = AbortController(); - - await isInitialized; - - final crudStream = - database.onChange(['ps_crud'], throttle: crudThrottleTime); - final storage = BucketStorage(database); StreamingSync sync; // Try using a shared worker for the synchronization implementation to avoid // duplicating work across tabs. try { sync = await SyncWorkerHandle.start( - database: this, - connector: connector, - crudThrottleTimeMs: crudThrottleTime.inMilliseconds, - workerUri: Uri.base.resolve('/powersync_sync.worker.js'), - syncParams: params); + database: this, + connector: connector, + options: options.source, + workerUri: Uri.base.resolve('/powersync_sync.worker.js'), + subscriptions: initiallyActiveStreams, + ); } catch (e) { logger.warning( 'Could not use shared worker for synchronization, falling back to locks.', e, ); + final crudStream = + database.onChange(['ps_crud'], throttle: options.crudThrottleTime); sync = StreamingSyncImplementation( adapter: storage, - credentialsCallback: connector.getCredentialsCached, - invalidCredentialsCallback: connector.fetchCredentials, - uploadCrud: () => connector.uploadData(this), + schemaJson: jsonEncode(schema), + connector: InternalConnector.wrap(connector, this), crudUpdateTriggerStream: crudStream, - retryDelay: Duration(seconds: 3), - client: FetchClient(mode: RequestMode.cors), - syncParameters: params, + options: options, + client: BrowserClient(), + activeSubscriptions: initiallyActiveStreams, // Only allows 1 sync implementation to run at a time per database // This should be global (across tabs) when using Navigator locks. identifier: database.openFactory.path, @@ -178,9 +171,13 @@ class PowerSyncDatabaseImpl setStatus(event); }); sync.streamingSync(); - disconnecter?.onAbort.then((_) async { + + final subscriptions = activeStreams.listen(sync.updateSubscriptions); + + abort.onAbort.then((_) async { + subscriptions.cancel(); await sync.abort(); - disconnecter?.completeAbort(); + abort.completeAbort(); }).ignore(); } @@ -226,14 +223,4 @@ class PowerSyncDatabaseImpl await isInitialized; return database.writeTransaction(callback, lockTimeout: lockTimeout); } - - @override - Future updateSchema(Schema schema) { - if (disconnecter != null) { - throw AssertionError('Cannot update schema while connected'); - } - schema.validate(); - this.schema = schema; - return database.writeLock((tx) => schema_logic.updateSchema(tx, schema)); - } } diff --git a/packages/powersync_core/lib/src/exceptions.dart b/packages/powersync_core/lib/src/exceptions.dart new file mode 100644 index 00000000..bc35df4a --- /dev/null +++ b/packages/powersync_core/lib/src/exceptions.dart @@ -0,0 +1,128 @@ +import 'dart:async'; +import 'dart:convert' as convert; + +import 'package:http/http.dart' as http; + +/// This indicates an error with configured credentials. +class CredentialsException implements Exception { + String message; + + CredentialsException(this.message); + + @override + toString() { + return 'CredentialsException: $message'; + } +} + +/// An internal protocol exception. +/// +/// This indicates that the server sent an invalid response. +class PowerSyncProtocolException implements Exception { + String message; + + PowerSyncProtocolException(this.message); + + @override + toString() { + return 'SyncProtocolException: $message'; + } +} + +/// An error that received from the sync service. +/// +/// Examples include authorization errors (401) and temporary service issues (503). +class SyncResponseException implements Exception { + /// Parse an error response from the PowerSync service + static Future fromStreamedResponse( + http.StreamedResponse response) async { + try { + final body = await response.stream.bytesToString(); + return _fromResponseBody(response, body); + } on Exception catch (_) { + // Could be FormatException, stream issues, or possibly other exceptions. + // Fallback to just using the response header. + return _fromResponseHeader(response); + } + } + + /// Parse an error response from the PowerSync service + static SyncResponseException fromResponse(http.Response response) { + try { + final body = response.body; + return _fromResponseBody(response, body); + } on Exception catch (_) { + // Could be FormatException, or possibly other exceptions. + // Fallback to just using the response header. + return _fromResponseHeader(response); + } + } + + static SyncResponseException _fromResponseBody( + http.BaseResponse response, String body) { + final decoded = convert.jsonDecode(body); + final details = switch (decoded['error']) { + final Map details => _errorDescription(details), + _ => null, + } ?? + body; + + final message = '${response.reasonPhrase ?? "Request failed"}: $details'; + return SyncResponseException(response.statusCode, message); + } + + static SyncResponseException _fromResponseHeader(http.BaseResponse response) { + return SyncResponseException( + response.statusCode, + response.reasonPhrase ?? "Request failed", + ); + } + + /// Extracts an error description from an error resonse looking like + /// `{"code":"PSYNC_S2106","status":401,"description":"Authentication required","name":"AuthorizationError"}`. + static String? _errorDescription(Map raw) { + final code = raw['code']; // Required, string + final description = raw['description']; // Required, string + + final name = raw['name']; // Optional, string + final details = raw['details']; // Optional, string + + if (code is! String || description is! String) { + return null; + } + + final fullDescription = StringBuffer(code); + if (name is String) { + fullDescription.write('($name)'); + } + + fullDescription + ..write(': ') + ..write(description); + + if (details is String) { + fullDescription + ..write(', ') + ..write(details); + } + + return fullDescription.toString(); + } + + int statusCode; + String description; + + SyncResponseException(this.statusCode, this.description); + + @override + toString() { + return 'SyncResponseException: $statusCode $description'; + } +} + +class PowersyncNotReadyException implements Exception { + /// @nodoc + PowersyncNotReadyException(this.message); + + final String message; +} diff --git a/packages/powersync/lib/src/isolate_completer.dart b/packages/powersync_core/lib/src/isolate_completer.dart similarity index 93% rename from packages/powersync/lib/src/isolate_completer.dart rename to packages/powersync_core/lib/src/isolate_completer.dart index 7307ce8a..12a5c543 100644 --- a/packages/powersync/lib/src/isolate_completer.dart +++ b/packages/powersync_core/lib/src/isolate_completer.dart @@ -26,7 +26,7 @@ class IsolateResult { return PortCompleter(receivePort.sendPort); } - close() { + void close() { receivePort.close(); } } @@ -43,16 +43,13 @@ class PortCompleter { } void completeError(Object error, [StackTrace? stackTrace]) { - sendPort.send(PortResult.error(error, stackTrace)); - } - - addExitHandler() { - Isolate.current.addOnExitListener(sendPort, response: abortedResponse); + sendPort.send(PortResult.error(error, stackTrace)); } Future handle(FutureOr Function() callback, {bool ignoreStackTrace = false}) async { - addExitHandler(); + Isolate.current.addOnExitListener(sendPort, response: abortedResponse); + try { final result = await callback(); complete(result); @@ -62,6 +59,8 @@ class PortCompleter { } else { completeError(error, stacktrace); } + } finally { + Isolate.current.removeOnExitListener(sendPort); } } } diff --git a/packages/powersync/lib/src/log.dart b/packages/powersync_core/lib/src/log.dart similarity index 95% rename from packages/powersync/lib/src/log.dart rename to packages/powersync_core/lib/src/log.dart index c628295b..0d0ef1ce 100644 --- a/packages/powersync/lib/src/log.dart +++ b/packages/powersync_core/lib/src/log.dart @@ -1,5 +1,5 @@ import 'package:logging/logging.dart'; -import 'package:powersync/src/log_internal.dart'; +import 'package:powersync_core/src/log_internal.dart'; /// Logger that outputs to the console in debug mode, and nothing /// in release and profile modes. diff --git a/packages/powersync/lib/src/log_internal.dart b/packages/powersync_core/lib/src/log_internal.dart similarity index 100% rename from packages/powersync/lib/src/log_internal.dart rename to packages/powersync_core/lib/src/log_internal.dart diff --git a/packages/powersync/lib/src/open_factory.dart b/packages/powersync_core/lib/src/open_factory.dart similarity index 66% rename from packages/powersync/lib/src/open_factory.dart rename to packages/powersync_core/lib/src/open_factory.dart index c32daf83..25a14643 100644 --- a/packages/powersync/lib/src/open_factory.dart +++ b/packages/powersync_core/lib/src/open_factory.dart @@ -2,8 +2,8 @@ // To conditionally export an implementation for either web or "native" platforms // The sqlite library uses dart:ffi which is not supported on web -export './open_factory/open_factory_stub.dart' +export 'open_factory/open_factory_stub.dart' // ignore: uri_does_not_exist - if (dart.library.io) './open_factory/native/native_open_factory.dart' + if (dart.library.io) 'open_factory/native/native_open_factory.dart' // ignore: uri_does_not_exist - if (dart.library.html) './open_factory/web/web_open_factory.dart'; + if (dart.library.js_interop) 'open_factory/web/web_open_factory.dart'; diff --git a/packages/powersync/lib/src/open_factory/abstract_powersync_open_factory.dart b/packages/powersync_core/lib/src/open_factory/abstract_powersync_open_factory.dart similarity index 89% rename from packages/powersync/lib/src/open_factory/abstract_powersync_open_factory.dart rename to packages/powersync_core/lib/src/open_factory/abstract_powersync_open_factory.dart index 06e8feda..cb88af22 100644 --- a/packages/powersync/lib/src/open_factory/abstract_powersync_open_factory.dart +++ b/packages/powersync_core/lib/src/open_factory/abstract_powersync_open_factory.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'package:universal_io/io.dart'; import 'dart:math'; -import 'package:powersync/sqlite_async.dart'; -import 'package:powersync/src/open_factory/common_db_functions.dart'; +import 'package:meta/meta.dart'; +import 'package:powersync_core/sqlite_async.dart'; +import 'package:powersync_core/src/open_factory/common_db_functions.dart'; import 'package:sqlite_async/sqlite3_common.dart'; const powerSyncDefaultSqliteOptions = SqliteOptions( @@ -70,6 +70,11 @@ abstract class AbstractPowerSyncOpenFactory extends DefaultSqliteOpenFactory { /// Returns the library name for the current platform. /// [path] is optional and is used when the library is not in the default location. String getLibraryForPlatform({String? path}); + + /// On native platforms, synchronously pauses the current isolate for the + /// given [Duration]. + @visibleForOverriding + void sleep(Duration duration) {} } /// Advanced: Define custom setup for each SQLite connection. diff --git a/packages/powersync/lib/src/open_factory/common_db_functions.dart b/packages/powersync_core/lib/src/open_factory/common_db_functions.dart similarity index 100% rename from packages/powersync/lib/src/open_factory/common_db_functions.dart rename to packages/powersync_core/lib/src/open_factory/common_db_functions.dart diff --git a/packages/powersync/lib/src/open_factory/native/native_open_factory.dart b/packages/powersync_core/lib/src/open_factory/native/native_open_factory.dart similarity index 91% rename from packages/powersync/lib/src/open_factory/native/native_open_factory.dart rename to packages/powersync_core/lib/src/open_factory/native/native_open_factory.dart index e2cbd575..775e59e8 100644 --- a/packages/powersync/lib/src/open_factory/native/native_open_factory.dart +++ b/packages/powersync_core/lib/src/open_factory/native/native_open_factory.dart @@ -1,9 +1,11 @@ +import 'dart:io'; +import 'dart:io' as io; import 'dart:ffi'; -import 'package:powersync/powersync.dart'; -import 'package:universal_io/io.dart'; +import 'package:powersync_core/src/exceptions.dart'; +import 'package:powersync_core/src/log.dart'; import 'dart:isolate'; -import 'package:powersync/src/open_factory/abstract_powersync_open_factory.dart'; +import 'package:powersync_core/src/open_factory/abstract_powersync_open_factory.dart'; import 'package:sqlite_async/sqlite3.dart' as sqlite; import 'package:sqlite_async/sqlite3_common.dart'; import 'package:sqlite_async/sqlite_async.dart'; @@ -108,4 +110,9 @@ class PowerSyncOpenFactory extends AbstractPowerSyncOpenFactory { ); } } + + @override + void sleep(Duration duration) { + io.sleep(duration); + } } diff --git a/packages/powersync/lib/src/open_factory/open_factory_stub.dart b/packages/powersync_core/lib/src/open_factory/open_factory_stub.dart similarity index 100% rename from packages/powersync/lib/src/open_factory/open_factory_stub.dart rename to packages/powersync_core/lib/src/open_factory/open_factory_stub.dart diff --git a/packages/powersync/lib/src/open_factory/web/web_open_factory.dart b/packages/powersync_core/lib/src/open_factory/web/web_open_factory.dart similarity index 56% rename from packages/powersync/lib/src/open_factory/web/web_open_factory.dart rename to packages/powersync_core/lib/src/open_factory/web/web_open_factory.dart index 8c137393..db099cb8 100644 --- a/packages/powersync/lib/src/open_factory/web/web_open_factory.dart +++ b/packages/powersync_core/lib/src/open_factory/web/web_open_factory.dart @@ -1,17 +1,38 @@ import 'dart:async'; -import 'package:powersync/src/open_factory/abstract_powersync_open_factory.dart'; -import 'package:powersync/src/uuid.dart'; +import 'package:powersync_core/src/open_factory/abstract_powersync_open_factory.dart'; +import 'package:powersync_core/src/uuid.dart'; import 'package:sqlite_async/sqlite3_common.dart'; +import 'package:sqlite_async/sqlite3_web.dart'; import 'package:sqlite_async/sqlite_async.dart'; +import 'package:sqlite_async/web.dart'; + +import '../../web/worker_utils.dart'; /// Web implementation for [AbstractPowerSyncOpenFactory] -class PowerSyncOpenFactory extends AbstractPowerSyncOpenFactory { +class PowerSyncOpenFactory extends AbstractPowerSyncOpenFactory + with WebSqliteOpenFactory { PowerSyncOpenFactory({ required super.path, super.sqliteOptions, }); + @override + Future openWebSqlite(WebSqliteOptions options) async { + return WebSqlite.open( + wasmModule: Uri.parse(sqliteOptions.webSqliteOptions.wasmUri), + worker: Uri.parse(sqliteOptions.webSqliteOptions.workerUri), + controller: PowerSyncAsyncSqliteController(), + handleCustomRequest: handleCustomRequest, + ); + } + + @override + Future connectToWorker( + WebSqlite sqlite, String name) { + return sqlite.connectToRecommended(name); + } + @override void enableExtension() { // No op for web @@ -20,11 +41,11 @@ class PowerSyncOpenFactory extends AbstractPowerSyncOpenFactory { @override Future openConnection(SqliteOpenOptions options) async { var conn = await super.openConnection(options); - for (final statement in super.pragmaStatements(options)) { + for (final statement in pragmaStatements(options)) { await conn.execute(statement); } - return super.openConnection(options); + return conn; } @override diff --git a/packages/powersync/lib/src/powersync_update_notification.dart b/packages/powersync_core/lib/src/powersync_update_notification.dart similarity index 94% rename from packages/powersync/lib/src/powersync_update_notification.dart rename to packages/powersync_core/lib/src/powersync_update_notification.dart index b97a27ce..44b03078 100644 --- a/packages/powersync/lib/src/powersync_update_notification.dart +++ b/packages/powersync_core/lib/src/powersync_update_notification.dart @@ -45,8 +45,7 @@ class PowerSyncUpdateNotification extends UpdateNotification { Set _friendlyTableNames(Iterable originalTables) { Set tables = {}; for (var table in originalTables) { - var friendlyName = friendlyTableName(table); - if (friendlyName != null) { + if (friendlyTableName(table) case final friendlyName?) { tables.add(friendlyName); } else if (!table.startsWith('ps_')) { tables.add(table); diff --git a/packages/powersync_core/lib/src/schema.dart b/packages/powersync_core/lib/src/schema.dart new file mode 100644 index 00000000..1289cae0 --- /dev/null +++ b/packages/powersync_core/lib/src/schema.dart @@ -0,0 +1,463 @@ +import 'crud.dart'; +import 'schema_logic.dart'; + +/// The schema used by the database. +/// +/// The implementation uses the schema as a "VIEW" on top of JSON data. +/// No migrations are required on the client. +class Schema { + /// List of tables in the schema. + /// + /// When opening a PowerSync database, these tables will be created and + /// migrated automatically. + final List
tables; + + /// A list of [RawTable]s in addition to PowerSync-managed [tables]. + /// + /// Raw tables give users full control over the SQLite tables, but that + /// includes the responsibility to create those tables and to write migrations + /// for them. + /// + /// For more information on raw tables, see [RawTable] and [the documentation](https://docs.powersync.com/usage/use-case-examples/raw-tables). + final List rawTables; + + const Schema(this.tables, {this.rawTables = const []}); + + Map toJson() => {'raw_tables': rawTables, 'tables': tables}; + + void validate() { + Set tableNames = {}; + for (var table in tables) { + table.validate(); + + if (tableNames.contains(table.name)) { + throw AssertionError("Duplicate table name: ${table.name}"); + } + + tableNames.add(table.name); + } + } +} + +/// Options to include old values in [CrudEntry] for update statements. +/// +/// These options are enabled by passing them to a non-local [Table] +/// constructor. +final class TrackPreviousValuesOptions { + /// A filter of column names for which updates should be tracked. + /// + /// When set to a non-null value, columns not included in this list will not + /// appear in [CrudEntry.previousValues]. By default, all columns are + /// included. + final List? columnFilter; + + /// Whether to only include old values when they were changed by an update, + /// instead of always including all old values. + final bool onlyWhenChanged; + + const TrackPreviousValuesOptions( + {this.columnFilter, this.onlyWhenChanged = false}); +} + +/// A single table in the schema. +class Table { + static const _maxNumberOfColumns = 1999; + + /// The synced table name, matching sync rules. + final String name; + + /// List of columns. + final List columns; + + /// List of indexes. + final List indexes; + + /// Whether to add a hidden `_metadata` column that will be enabled for + /// updates to attach custom information about writes that will be reported + /// through [CrudEntry.metadata]. + final bool trackMetadata; + + /// Whether to track old values of columns for [CrudEntry.previousValues]. + /// + /// See [TrackPreviousValuesOptions] for details. + final TrackPreviousValuesOptions? trackPreviousValues; + + /// Whether the table only exists locally. + final bool localOnly; + + /// Whether this is an insert-only table. + final bool insertOnly; + + /// Whether an `UPDATE` statement that doesn't change any values should be + /// ignored when creating CRUD entries. + final bool ignoreEmptyUpdates; + + /// Override the name for the view + final String? _viewNameOverride; + + /// powersync-sqlite-core limits the number of columns + /// per table to 1999, due to internal SQLite limits. + /// + /// In earlier versions this was limited to 63. + final int maxNumberOfColumns = _maxNumberOfColumns; + + /// Internal use only. + /// + /// Name of the table that stores the underlying data. + String get internalName { + if (localOnly) { + return "ps_data_local__$name"; + } else { + return "ps_data__$name"; + } + } + + /// Create a synced table. + /// + /// Local changes are recorded, and remote changes are synced to the local table. + const Table( + this.name, + this.columns, { + this.indexes = const [], + String? viewName, + this.localOnly = false, + this.ignoreEmptyUpdates = false, + this.trackMetadata = false, + this.trackPreviousValues, + }) : insertOnly = false, + _viewNameOverride = viewName; + + /// Create a table that only exists locally. + /// + /// This table does not record changes, and is not synchronized from the service. + const Table.localOnly(this.name, this.columns, + {this.indexes = const [], String? viewName}) + : localOnly = true, + insertOnly = false, + trackMetadata = false, + trackPreviousValues = null, + ignoreEmptyUpdates = false, + _viewNameOverride = viewName; + + /// Create a table that only supports inserts. + /// + /// This table supports INSERT statements, operations are recorded internally + /// and are cleared once handled in the `PowerSyncBackendConnector.uploadData` + /// method. + /// + /// SELECT queries on the table will always return 0 rows. + /// + const Table.insertOnly( + this.name, + this.columns, { + String? viewName, + this.ignoreEmptyUpdates = false, + this.trackMetadata = false, + this.trackPreviousValues, + }) : localOnly = false, + insertOnly = true, + indexes = const [], + _viewNameOverride = viewName; + + Column operator [](String columnName) { + return columns.firstWhere((element) => element.name == columnName); + } + + bool get validName { + return !invalidSqliteCharacters.hasMatch(name) && + (_viewNameOverride == null || + !invalidSqliteCharacters.hasMatch(_viewNameOverride)); + } + + /// Check that there are no issues in the table definition. + void validate() { + if (columns.length > _maxNumberOfColumns) { + throw AssertionError( + "Table $name has more than $_maxNumberOfColumns columns, which is not supported"); + } + + if (invalidSqliteCharacters.hasMatch(name)) { + throw AssertionError("Invalid characters in table name: $name"); + } + + if (_viewNameOverride != null && + invalidSqliteCharacters.hasMatch(_viewNameOverride)) { + throw AssertionError( + "Invalid characters in view name: $_viewNameOverride"); + } + + if (trackMetadata && localOnly) { + throw AssertionError("Local-only tables can't track metadata"); + } + + if (trackPreviousValues != null && localOnly) { + throw AssertionError("Local-only tables can't track old values"); + } + + Set columnNames = {"id"}; + for (var column in columns) { + if (column.name == 'id') { + throw AssertionError( + "$name: id column is automatically added, custom id columns are not supported"); + } else if (columnNames.contains(column.name)) { + throw AssertionError("Duplicate column $name.${column.name}"); + } else if (invalidSqliteCharacters.hasMatch(column.name)) { + throw AssertionError( + "Invalid characters in column name: $name.${column.name}"); + } + + columnNames.add(column.name); + } + Set indexNames = {}; + + for (var index in indexes) { + if (indexNames.contains(index.name)) { + throw AssertionError("Duplicate index $name.${index.name}"); + } else if (invalidSqliteCharacters.hasMatch(index.name)) { + throw AssertionError( + "Invalid characters in index name: $name.${index.name}"); + } + + for (var column in index.columns) { + if (!columnNames.contains(column.column)) { + throw AssertionError( + "Column $name.${column.column} not found for index ${index.name}"); + } + } + + indexNames.add(index.name); + } + } + + /// Name for the view, used for queries. + /// Defaults to the synced table name. + String get viewName { + return _viewNameOverride ?? name; + } + + Map toJson() => { + 'name': name, + 'view_name': _viewNameOverride, + 'local_only': localOnly, + 'insert_only': insertOnly, + 'columns': columns, + 'indexes': indexes.map((e) => e.toJson(this)).toList(growable: false), + 'ignore_empty_update': ignoreEmptyUpdates, + 'include_metadata': trackMetadata, + if (trackPreviousValues case final trackPreviousValues?) ...{ + 'include_old': trackPreviousValues.columnFilter ?? true, + 'include_old_only_when_changed': trackPreviousValues.onlyWhenChanged, + }, + }; +} + +class Index { + /// Descriptive name of the index. + final String name; + + /// List of columns used for the index. + final List columns; + + /// Construct a new index with the specified columns. + const Index(this.name, this.columns); + + /// Construct a new index with the specified column names. + factory Index.ascending(String name, List columns) { + return Index(name, + columns.map((e) => IndexedColumn.ascending(e)).toList(growable: false)); + } + + /// Internal use only. + /// + /// Specifies the full name of this index on a table. + String fullName(Table table) { + return "${table.internalName}__$name"; + } + + Map toJson(Table table) => { + 'name': name, + 'columns': columns.map((c) => c.toJson(table)).toList(growable: false) + }; +} + +/// Describes an indexed column. +class IndexedColumn { + /// Name of the column to index. + final String column; + + /// Whether this column is stored in ascending order in the index. + final bool ascending; + + const IndexedColumn(this.column, {this.ascending = true}); + const IndexedColumn.ascending(this.column) : ascending = true; + const IndexedColumn.descending(this.column) : ascending = false; + + Map toJson(Table table) { + final t = table[column].type; + + return {'name': column, 'ascending': ascending, 'type': t.sqlite}; + } +} + +/// A single column in a table schema. +class Column { + /// Name of the column. + final String name; + + /// Type of the column. + /// + /// If the underlying data does not match this type, + /// it is cast automatically. + /// + /// For details on the cast, see: + /// https://www.sqlite.org/lang_expr.html#castexpr + final ColumnType type; + + const Column(this.name, this.type); + + /// Create a TEXT column. + const Column.text(this.name) : type = ColumnType.text; + + /// Create an INTEGER column. + const Column.integer(this.name) : type = ColumnType.integer; + + /// Create a REAL column. + const Column.real(this.name) : type = ColumnType.real; + + Map toJson() => {'name': name, 'type': type.sqlite}; +} + +/// A raw table, defined by the user instead of being managed by PowerSync. +/// +/// Any ordinary SQLite table can be defined as a raw table, which enables: +/// +/// - More performant queries, since data is stored in typed rows instead of the +/// schemaless JSON view PowerSync uses by default. +/// - More control over the table, since custom column constraints can be used +/// in its definition. +/// +/// PowerSync doesn't know anything about the internal structure of raw tables - +/// instead, it relies on user-defined [put] and [delete] statements to sync +/// data into them. +/// +/// When using raw tables, you are responsible for creating and migrating them +/// when they've changed. Further, triggers are necessary to collect local +/// writes to those tables. For more information, see +/// [the documentation](https://docs.powersync.com/usage/use-case-examples/raw-tables). +/// +/// Note that raw tables are only supported by the Rust sync client, which needs +/// to be enabled when connecting with raw tables. +final class RawTable { + /// The name of the table as used by the sync service. + /// + /// This doesn't necessarily have to match the name of the SQLite table that + /// [put] and [delete] write to. Instead, it's used by the sync client to + /// identify which statements to use when it encounters sync operations for + /// this table. + final String name; + + /// A statement responsible for inserting or updating a row in this raw table + /// based on data from the sync service. + /// + /// See [PendingStatement] for details. + final PendingStatement put; + + /// A statement responsible for deleting a row based on its PowerSync id. + /// + /// See [PendingStatement] for details. Note that [PendingStatementValue]s + /// used here must all be [PendingStatementValue.id]. + final PendingStatement delete; + + const RawTable({ + required this.name, + required this.put, + required this.delete, + }); + + Map toJson() => { + 'name': name, + 'put': put, + 'delete': delete, + }; +} + +/// An SQL statement to be run by the sync client against raw tables. +/// +/// Since raw tables are managed by the user, PowerSync can't know how to apply +/// serverside changes to them. These statements bridge raw tables and PowerSync +/// by providing upserts and delete statements. +/// +/// For more information, see [the documentation](https://docs.powersync.com/usage/use-case-examples/raw-tables) +final class PendingStatement { + /// The SQL statement to run to upsert or delete data from a raw table. + final String sql; + + /// A list of value identifiers for parameters in [sql]. + /// + /// Put statements can use both [PendingStatementValue.id] and + /// [PendingStatementValue.column], whereas delete statements can only use + /// [PendingStatementValue.id]. + final List params; + + PendingStatement({required this.sql, required this.params}); + + Map toJson() => { + 'sql': sql, + 'params': params, + }; +} + +/// A description of a value that will be resolved in the sync client when +/// running a [PendingStatement] for a [RawTable]. +sealed class PendingStatementValue { + /// A value that is bound to the textual id used in the PowerSync protocol. + factory PendingStatementValue.id() = _PendingStmtValueId; + + /// A value that is bound to the value of a column in a replace (`PUT`) + /// operation of the PowerSync protocol. + factory PendingStatementValue.column(String column) = _PendingStmtValueColumn; + + dynamic toJson(); +} + +class _PendingStmtValueColumn implements PendingStatementValue { + final String column; + const _PendingStmtValueColumn(this.column); + + @override + dynamic toJson() { + return { + 'Column': column, + }; + } +} + +class _PendingStmtValueId implements PendingStatementValue { + const _PendingStmtValueId(); + + @override + dynamic toJson() { + return 'Id'; + } +} + +/// Type of column. +enum ColumnType { + /// TEXT column. + text('TEXT'), + + /// INTEGER column. + integer('INTEGER'), + + /// REAL column. + real('REAL'); + + final String sqlite; + + const ColumnType(this.sqlite); + + @override + toString() { + return sqlite; + } +} diff --git a/packages/powersync/lib/src/schema_logic.dart b/packages/powersync_core/lib/src/schema_logic.dart similarity index 70% rename from packages/powersync/lib/src/schema_logic.dart rename to packages/powersync_core/lib/src/schema_logic.dart index 93296c5a..e9064ea9 100644 --- a/packages/powersync/lib/src/schema_logic.dart +++ b/packages/powersync_core/lib/src/schema_logic.dart @@ -21,8 +21,14 @@ Future updateSchemaInIsolate( } String? friendlyTableName(String table) { - final re = RegExp(r"^ps_data__(.+)$"); - final re2 = RegExp(r"^ps_data_local__(.+)$"); - final match = re.firstMatch(table) ?? re2.firstMatch(table); - return match?.group(1); + const prefix1 = 'ps_data__'; + const prefix2 = 'ps_data_local__'; + + if (table.startsWith(prefix2)) { + return table.substring(prefix2.length); + } else if (table.startsWith(prefix1)) { + return table.substring(prefix1.length); + } else { + return null; + } } diff --git a/packages/powersync_core/lib/src/setup_web.dart b/packages/powersync_core/lib/src/setup_web.dart new file mode 100644 index 00000000..1886c3b0 --- /dev/null +++ b/packages/powersync_core/lib/src/setup_web.dart @@ -0,0 +1,245 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:collection/collection.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:args/args.dart'; +import 'package:path/path.dart' as p; + +Future downloadWebAssets(List arguments, + {bool encryption = false}) async { + var parser = ArgParser(); + // Add a flag to enable/disable the download of worker (defaults to true) + // Pass the --no-worker argument to disable the download of the worker + // dart run powersync:setup_web --no-worker + parser.addFlag('worker', defaultsTo: true); + // Add a option to specify the output directory (defaults to web) + // Pass the --output-dir argument to specify the output directory + // dart run powersync:setup_web --output-dir assets + parser.addOption('output-dir', abbr: 'o', defaultsTo: 'web'); + var results = parser.parse(arguments); + bool downloadWorker = results.flag('worker'); + String outputDir = results.option('output-dir')!; + + final root = Directory.current.uri; + print('Project root: ${root.toFilePath()}'); + + final wasmFileName = encryption ? 'sqlite3mc.wasm' : 'sqlite3.wasm'; + final wasmPath = '${root.toFilePath()}$outputDir/$wasmFileName'; + + final workerPath = '${root.toFilePath()}$outputDir/powersync_db.worker.js'; + final syncWorkerPath = + '${root.toFilePath()}$outputDir/powersync_sync.worker.js'; + + final packageConfigFile = File.fromUri( + root.resolve('.dart_tool/package_config.json'), + ); + dynamic packageConfig; + try { + packageConfig = json.decode(await packageConfigFile.readAsString()); + } on FileSystemException { + print('Missing .dart_tool/package_config.json'); + print('Run `flutter pub get` first.'); + exit(1); + } on FormatException { + print('Invalid .dart_tool/package_config.json'); + print('Run `flutter pub get` first.'); + exit(1); + } + + if (Platform.environment.containsKey('IS_IN_POWERSYNC_CI')) { + print('IS_IN_POWERSYNC_CI env variable is set, copying from local build'); + return _copyPrecompiled(Directory.current, wasmFileName, outputDir); + } + + try { + final httpClient = HttpClient(); + const sqlitePackageName = 'sqlite3'; + + final (tag: powersyncTag, version: powerSyncVersion) = + await powerSyncVersionOrLatest( + httpClient, packageConfig, packageConfigFile); + final firstPowerSyncVersionWithOwnWasm = Version(1, 12, 0); + + if (downloadWorker) { + final workerUrl = + 'https://github.com/powersync-ja/powersync.dart/releases/download/$powersyncTag/powersync_db.worker.js'; + final syncWorkerUrl = + 'https://github.com/powersync-ja/powersync.dart/releases/download/$powersyncTag/powersync_sync.worker.js'; + + await downloadFile(httpClient, workerUrl, workerPath); + await downloadFile(httpClient, syncWorkerUrl, syncWorkerPath); + } + + if (powerSyncVersion >= firstPowerSyncVersionWithOwnWasm) { + final wasmUrl = + 'https://github.com/powersync-ja/powersync.dart/releases/download/$powersyncTag/$wasmFileName'; + + await downloadFile(httpClient, wasmUrl, wasmPath); + } else { + final sqlite3Pkg = getPackageFromConfig(packageConfig, sqlitePackageName); + + String sqlite3Version = + "v${getPubspecVersion(packageConfigFile, sqlite3Pkg, sqlitePackageName)}"; + + List tags = await getLatestTagsFromRelease(httpClient); + String? matchTag = tags.firstWhereOrNull((element) => + element.contains(sqlite3Version) && coreVersionIsInRange(element)); + if (matchTag != null) { + sqlite3Version = matchTag; + } else { + throw Exception( + """No compatible powersync core version found for sqlite3 version $sqlite3Version + Latest supported sqlite3 versions: ${tags.take(3).map((tag) => tag.split('-')[0]).join(', ')}. + You can view the full list of releases at https://github.com/powersync-ja/sqlite3.dart/releases"""); + } + + final sqliteUrl = + 'https://github.com/powersync-ja/sqlite3.dart/releases/download/$sqlite3Version/$wasmFileName'; + + await downloadFile(httpClient, sqliteUrl, wasmPath); + } + } catch (e) { + print(e); + exit(1); + } +} + +Future<({String tag, Version version})> powerSyncVersionOrLatest( + HttpClient client, dynamic packageConfig, File packageConfigFile) async { + const powersyncPackageName = 'powersync'; + // Don't require powersync dependency. The user has one if running this script + // and we also want to support powersync_sqlcipher (for which we download + // the latest versions). + final powersyncPkg = getPackageFromConfig(packageConfig, powersyncPackageName, + required: false); + if (powersyncPkg == null) { + final [tag, ...] = + await getLatestTagsFromRelease(client, repo: 'powersync.dart'); + + return ( + tag: tag, + version: Version.parse(tag.substring('powersync-v'.length)) + ); + } + + final powersyncVersion = + getPubspecVersion(packageConfigFile, powersyncPkg, powersyncPackageName); + return ( + tag: 'powersync-v$powersyncVersion', + version: Version.parse(powersyncVersion), + ); +} + +bool coreVersionIsInRange(String tag) { + // Sets the range of powersync core version that is compatible with the sqlite3 version + // We're a little more selective in the versions chosen here than the range + // we're compatible with. + VersionConstraint constraint = VersionConstraint.parse('>=0.3.10 <0.4.0'); + List parts = tag.split('-'); + String powersyncPart = parts[1]; + + List versionParts = powersyncPart.split('.'); + String extractedVersion = + versionParts.sublist(versionParts.length - 3).join('.'); + final coreVersion = Version.parse(extractedVersion); + if (constraint.allows(coreVersion)) { + return true; + } + return false; +} + +dynamic getPackageFromConfig(dynamic packageConfig, String packageName, + {bool required = false}) { + final pkg = (packageConfig['packages'] as List? ?? []).firstWhere( + (dynamic e) => e['name'] == packageName, + orElse: () => null, + ); + if (pkg == null && required) { + throw Exception('Dependency on package:$packageName is required'); + } + return pkg; +} + +String getPubspecVersion( + File packageConfigFile, dynamic package, String packageName) { + final rootUri = + packageConfigFile.uri.resolve(package['rootUri'] as String? ?? ''); + print('Using package:$packageName from ${rootUri.toFilePath()}'); + + String pubspec = + File('${rootUri.toFilePath()}/pubspec.yaml').readAsStringSync(); + Pubspec parsed = Pubspec.parse(pubspec); + final version = parsed.version?.toString(); + if (version == null) { + throw Exception( + "${capitalize(packageName)} version not found. Run `flutter pub get` first."); + } + return version; +} + +String capitalize(String s) => s[0].toUpperCase() + s.substring(1); + +Future> getLatestTagsFromRelease(HttpClient httpClient, + {String repo = 'sqlite3.dart'}) async { + var request = await httpClient.getUrl( + Uri.parse("https://api.github.com/repos/powersync-ja/$repo/releases")); + var response = await request.close(); + if (response.statusCode == HttpStatus.ok) { + var res = await response.transform(utf8.decoder).join(); + var jsonObj = json.decode(res) as List; + List tags = []; + for (dynamic obj in jsonObj) { + final tagName = obj['tag_name'] as String; + if (!tagName.contains("powersync")) continue; + tags.add(tagName); + } + return tags; + } else { + throw Exception("Failed to fetch GitHub releases and tags"); + } +} + +Future downloadFile( + HttpClient httpClient, String url, String savePath) async { + print('Downloading: $url'); + var request = await httpClient.getUrl(Uri.parse(url)); + var response = await request.close(); + if (response.statusCode == HttpStatus.ok) { + var file = File(savePath); + await response.pipe(file.openWrite()); + } else { + throw Exception( + 'Failed to download file: ${response.statusCode} ${response.reasonPhrase}'); + } +} + +/// Copies WebAssembly modules from `packages/sqlite3_wasm_build/dist` into +/// `web/`. +/// +/// When we're running this setup script as part of our CI, a previous action +/// (`.github/actions/prepare/`) will have put compiled assets into that folder. +/// Copying from there ensures we run web tests against our current SQLite web +/// build and avoids downloading from GitHub releases for every package we test. +Future _copyPrecompiled( + Directory project, String wasmFile, String outputDir) async { + // Keep going up until we see the melos.yaml file indicating the workspace + // root. + var dir = project; + while (!await File(p.join(dir.path, 'melos.yaml')).exists()) { + print('Looking for melos workspace in $dir'); + final parent = dir.parent; + if (p.equals(parent.path, dir.path)) { + throw 'Melos workspace not found'; + } + + dir = parent; + } + + // In the CI, an earlier step will have put these files into the prepared + // sqlite3_wasm_build package. + final destination = p.join(project.path, outputDir); + final wasmSource = p.join(dir.path, 'packages', 'sqlite3_wasm_build', 'dist'); + print('Copying $wasmFile from $wasmSource to $destination'); + await File(p.join(wasmSource, wasmFile)).copy(p.join(destination, wasmFile)); +} diff --git a/packages/powersync/lib/src/bucket_storage.dart b/packages/powersync_core/lib/src/sync/bucket_storage.dart similarity index 61% rename from packages/powersync/lib/src/bucket_storage.dart rename to packages/powersync_core/lib/src/sync/bucket_storage.dart index ab26f654..88186776 100644 --- a/packages/powersync/lib/src/bucket_storage.dart +++ b/packages/powersync_core/lib/src/sync/bucket_storage.dart @@ -1,27 +1,31 @@ +@internal +library; + import 'dart:async'; import 'dart:convert'; import 'package:collection/collection.dart'; -import 'package:powersync/sqlite_async.dart'; -import 'package:powersync/sqlite3_common.dart'; +import 'package:meta/meta.dart'; +import 'package:powersync_core/sqlite_async.dart'; +import 'package:powersync_core/sqlite3_common.dart'; -import 'crud.dart'; -import 'schema_logic.dart'; -import 'sync_types.dart'; +import '../crud.dart'; +import '../schema_logic.dart'; +import 'protocol.dart'; const compactOperationInterval = 1000; +typedef LocalOperationCounters = ({int atLast, int sinceLast}); + class BucketStorage { final SqliteConnection _internalDb; bool _hasCompletedSync = false; - bool _pendingBucketDeletes = false; - int _compactCounter = compactOperationInterval; BucketStorage(SqliteConnection db) : _internalDb = db { _init(); } - _init() {} + void _init() {} // Use only for read statements Future select(String query, @@ -29,39 +33,50 @@ class BucketStorage { return await _internalDb.execute(query, parameters); } - void startSession() {} - Future> getBucketStates() async { final rows = await select( - 'SELECT name as bucket, cast(last_op as TEXT) as op_id FROM ps_buckets WHERE pending_delete = 0'); + "SELECT name, cast(last_op as TEXT) FROM ps_buckets WHERE pending_delete = 0 AND name != '\$local'"); return [ for (var row in rows) - BucketState(bucket: row['bucket'], opId: row['op_id']) + BucketState( + bucket: row.columnAt(0) as String, + opId: row.columnAt(1) as String, + ) ]; } + Future> + getBucketOperationProgress() async { + final rows = await select( + "SELECT name, count_at_last, count_since_last FROM ps_buckets"); + + return { + for (final row in rows) + (row.columnAt(0) as String): ( + atLast: row.columnAt(1) as int, + sinceLast: row.columnAt(2) as int, + ) + }; + } + Future getClientId() async { final rows = await select('SELECT powersync_client_id() as client_id'); return rows.first['client_id'] as String; } Future saveSyncData(SyncDataBatch batch) async { - var count = 0; - await writeTransaction((tx) async { for (var b in batch.buckets) { - count += b.data.length; await _updateBucket2( tx, jsonEncode({ - 'buckets': [b] + 'buckets': [b], })); } // No need to flush - the data is not directly visible to the user either way. // We get major initial sync performance improvements with IndexedDB by // not flushing here. }, flush: false); - _compactCounter += count; } Future _updateBucket2(SqliteWriteContext tx, String json) async { @@ -82,8 +97,6 @@ class BucketStorage { ['delete_bucket', bucket]); // No need to flush - not directly visible to the user }, flush: false); - - _pendingBucketDeletes = true; } Future hasCompletedSync() async { @@ -100,9 +113,9 @@ class BucketStorage { return false; } - Future syncLocalDatabase( - Checkpoint checkpoint) async { - final r = await validateChecksums(checkpoint); + Future syncLocalDatabase(Checkpoint checkpoint, + {int? forPriority}) async { + final r = await validateChecksums(checkpoint, priority: forPriority); if (!r.checkpointValid) { for (String b in r.checkpointFailures ?? []) { @@ -110,13 +123,16 @@ class BucketStorage { } return r; } - final bucketNames = [for (final c in checkpoint.checksums) c.bucket]; + final bucketNames = [ + for (final c in checkpoint.checksums) + if (forPriority == null || c.priority <= forPriority) c.bucket + ]; await writeTransaction((tx) async { await tx.execute( "UPDATE ps_buckets SET last_op = ? WHERE name IN (SELECT json_each.value FROM json_each(?))", [checkpoint.lastOpId, jsonEncode(bucketNames)]); - if (checkpoint.writeCheckpoint != null) { + if (forPriority == null && checkpoint.writeCheckpoint != null) { await tx.execute( "UPDATE ps_buckets SET last_op = ? WHERE name = '\$local'", [checkpoint.writeCheckpoint]); @@ -124,24 +140,52 @@ class BucketStorage { // Not flushing here - the flush will happen in the next step }, flush: false); - final valid = await updateObjectsFromBuckets(checkpoint); + final valid = await updateObjectsFromBuckets(checkpoint, + forPartialPriority: forPriority); if (!valid) { return SyncLocalDatabaseResult(ready: false); } - await forceCompact(); - return SyncLocalDatabaseResult(ready: true); } - Future updateObjectsFromBuckets(Checkpoint checkpoint) async { + Future updateObjectsFromBuckets(Checkpoint checkpoint, + {int? forPartialPriority}) async { return writeTransaction((tx) async { - await tx.execute( - "INSERT INTO powersync_operations(op, data) VALUES(?, ?)", - ['sync_local', '']); + await tx + .execute("INSERT INTO powersync_operations(op, data) VALUES(?, ?)", [ + 'sync_local', + forPartialPriority != null + ? jsonEncode({ + 'priority': forPartialPriority, + // If we're at a partial checkpoint, we should only publish the + // buckets at the completed priority levels. + 'buckets': [ + for (final desc in checkpoint.checksums) + // Note that higher priorities are encoded as smaller values + if (desc.priority <= forPartialPriority) desc.bucket, + ], + }) + : null, + ]); final rs = await tx.execute('SELECT last_insert_rowid() as result'); final result = rs[0]['result']; if (result == 1) { + if (forPartialPriority == null) { + // Reset progress counters. We only do this for a complete sync, as we + // want a download progress to always cover a complete checkpoint + // instead of resetting for partial completions. + await tx.execute(r''' +UPDATE ps_buckets SET count_since_last = 0, count_at_last = ?1->name + WHERE name != '$local' AND ?1->name IS NOT NULL +''', [ + json.encode({ + for (final bucket in checkpoint.checksums) + if (bucket.count case final count?) bucket.bucket: count, + }), + ]); + } + return true; } else { // can_update_local(db) == false @@ -153,67 +197,25 @@ class BucketStorage { }, flush: true); } - Future validateChecksums( - Checkpoint checkpoint) async { - final rs = await select("SELECT powersync_validate_checkpoint(?) as result", - [jsonEncode(checkpoint)]); - final result = jsonDecode(rs[0]['result']); - if (result['valid']) { + Future validateChecksums(Checkpoint checkpoint, + {int? priority}) async { + final rs = + await select("SELECT powersync_validate_checkpoint(?) as result", [ + jsonEncode({...checkpoint.toJson(priority: priority)}) + ]); + final result = + jsonDecode(rs[0]['result'] as String) as Map; + if (result['valid'] as bool) { return SyncLocalDatabaseResult(ready: true); } else { return SyncLocalDatabaseResult( checkpointValid: false, ready: false, - checkpointFailures: result['failed_buckets'].cast()); + checkpointFailures: + (result['failed_buckets'] as List).cast()); } } - Future forceCompact() async { - _compactCounter = compactOperationInterval; - _pendingBucketDeletes = true; - - await autoCompact(); - } - - Future autoCompact() async { - // This is a no-op since powersync-sqlite-core v0.3.0 - - // 1. Delete buckets - await _deletePendingBuckets(); - - // 2. Clear REMOVE operations, only keeping PUT ones - await _clearRemoveOps(); - } - - Future _deletePendingBuckets() async { - // This is a no-op since powersync-sqlite-core v0.3.0 - if (_pendingBucketDeletes) { - // Executed once after start-up, and again when there are pending deletes. - await writeTransaction((tx) async { - await tx.execute( - 'INSERT INTO powersync_operations(op, data) VALUES (?, ?)', - ['delete_pending_buckets', '']); - // No need to flush - not directly visible to the user - }, flush: false); - _pendingBucketDeletes = false; - } - } - - Future _clearRemoveOps() async { - if (_compactCounter < compactOperationInterval) { - return; - } - - // This is a no-op since powersync-sqlite-core v0.3.0 - await writeTransaction((tx) async { - await tx.execute( - 'INSERT INTO powersync_operations(op, data) VALUES (?, ?)', - ['clear_remove_ops', '']); - // No need to flush - not directly visible to the user - }, flush: false); - _compactCounter = 0; - } - void setTargetCheckpoint(Checkpoint checkpoint) { // No-op for now } @@ -232,7 +234,7 @@ class BucketStorage { // Nothing to update return false; } - int seqBefore = rs.first['seq']; + int seqBefore = rs.first['seq'] as int; var opId = await checkpointCallback(); return await writeTransaction((tx) async { @@ -244,7 +246,7 @@ class BucketStorage { .execute('SELECT seq FROM sqlite_sequence WHERE name = \'ps_crud\''); assert(rs.isNotEmpty); - int seqAfter = rs.first['seq']; + int seqAfter = rs.first['seq'] as int; if (seqAfter != seqBefore) { // New crud data may have been uploaded since we got the checkpoint. Abort. return false; @@ -307,6 +309,22 @@ class BucketStorage { }); } + Future control(String op, [Object? payload]) async { + return await writeTransaction( + (tx) async { + final [row] = + await tx.execute('SELECT powersync_control(?, ?)', [op, payload]); + return row.columnAt(0) as String; + }, + // We flush when powersync_control yields an instruction to do so. + flush: false, + ); + } + + Future flushFileSystem() async { + // Noop outside of web. + } + /// Note: The asynchronous nature of this is due to this needing a global /// lock. The actual database operations are still synchronous, and it /// is assumed that multiple functions on this instance won't be called @@ -341,111 +359,6 @@ class BucketState { } } -class SyncDataBatch { - List buckets; - - SyncDataBatch(this.buckets); -} - -class SyncBucketData { - final String bucket; - final List data; - final bool hasMore; - final String? after; - final String? nextAfter; - - const SyncBucketData( - {required this.bucket, - required this.data, - this.hasMore = false, - this.after, - this.nextAfter}); - - SyncBucketData.fromJson(Map json) - : bucket = json['bucket'], - hasMore = json['has_more'] ?? false, - after = json['after'], - nextAfter = json['next_after'], - data = - (json['data'] as List).map((e) => OplogEntry.fromJson(e)).toList(); - - Map toJson() { - return { - 'bucket': bucket, - 'has_more': hasMore, - 'after': after, - 'next_after': nextAfter, - 'data': data - }; - } -} - -class OplogEntry { - final String opId; - - final OpType? op; - - /// rowType + rowId uniquely identifies an entry in the local database. - final String? rowType; - final String? rowId; - - /// Together with rowType and rowId, this uniquely identifies a source entry - /// per bucket in the oplog. There may be multiple source entries for a single - /// "rowType + rowId" combination. - final String? subkey; - - final String? data; - final int checksum; - - const OplogEntry( - {required this.opId, - required this.op, - this.subkey, - this.rowType, - this.rowId, - this.data, - required this.checksum}); - - OplogEntry.fromJson(Map json) - : opId = json['op_id'], - op = OpType.fromJson(json['op']), - rowType = json['object_type'], - rowId = json['object_id'], - checksum = json['checksum'], - data = json['data'] is String ? json['data'] : jsonEncode(json['data']), - subkey = json['subkey'] is String ? json['subkey'] : null; - - Map? get parsedData { - return data == null ? null : jsonDecode(data!); - } - - /// Key to uniquely represent a source entry in a bucket. - /// This is used to supersede old entries. - /// Relevant for put and remove ops. - String get key { - return "$rowType/$rowId/$subkey"; - } - - Map toJson() { - return { - 'op_id': opId, - 'op': op?.toJson(), - 'object_type': rowType, - 'object_id': rowId, - 'checksum': checksum, - 'subkey': subkey, - 'data': data - }; - } -} - -class SqliteOp { - String sql; - List args; - - SqliteOp(this.sql, this.args); -} - class SyncLocalDatabaseResult { final bool ready; final bool checkpointValid; @@ -463,8 +376,8 @@ class SyncLocalDatabaseResult { @override int get hashCode { - return Object.hash( - ready, checkpointValid, const ListEquality().hash(checkpointFailures)); + return Object.hash(ready, checkpointValid, + const ListEquality().hash(checkpointFailures)); } @override @@ -472,7 +385,7 @@ class SyncLocalDatabaseResult { return other is SyncLocalDatabaseResult && other.ready == ready && other.checkpointValid == checkpointValid && - const ListEquality() + const ListEquality() .equals(other.checkpointFailures, checkpointFailures); } } @@ -519,8 +432,6 @@ enum OpType { return 'PUT'; case remove: return 'REMOVE'; - default: - return ''; } } } diff --git a/packages/powersync_core/lib/src/sync/connection_manager.dart b/packages/powersync_core/lib/src/sync/connection_manager.dart new file mode 100644 index 00000000..8a326642 --- /dev/null +++ b/packages/powersync_core/lib/src/sync/connection_manager.dart @@ -0,0 +1,396 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:meta/meta.dart'; +import 'package:powersync_core/src/abort_controller.dart'; +import 'package:powersync_core/src/connector.dart'; +import 'package:powersync_core/src/database/active_instances.dart'; +import 'package:powersync_core/src/database/powersync_db_mixin.dart'; +import 'package:powersync_core/src/sync/options.dart'; +import 'package:powersync_core/src/sync/stream.dart'; +import 'package:powersync_core/src/sync/sync_status.dart'; + +import 'instruction.dart'; +import 'mutable_sync_status.dart'; +import 'streaming_sync.dart'; + +/// A (stream name, JSON parameters) pair that uniquely identifies a stream +/// instantiation to subscribe to. +typedef _RawStreamKey = (String, String); + +@internal +final class ConnectionManager { + final PowerSyncDatabaseMixin db; + final ActiveDatabaseGroup _activeGroup; + + /// All streams (with parameters) for which a subscription has been requested + /// explicitly. + final Map<_RawStreamKey, _ActiveSubscription> _locallyActiveSubscriptions = + {}; + + final StreamController _statusController = + StreamController.broadcast(); + + /// Fires when an entry is added or removed from [_locallyActiveSubscriptions] + /// while we're connected. + StreamController? _subscriptionsChanged; + + SyncStatus _currentStatus = + const SyncStatus(connected: false, lastSyncedAt: null); + + SyncStatus get currentStatus => _currentStatus; + Stream get statusStream => _statusController.stream; + + /// The abort controller for the current sync iteration. + /// + /// null when disconnected, present when connecting or connected. + /// + /// The controller must only be accessed from within a critical section of the + /// sync mutex. + AbortController? _abortActiveSync; + + ConnectionManager(this.db) : _activeGroup = db.group; + + void checkNotConnected() { + if (_abortActiveSync != null) { + throw StateError('Cannot update schema while connected'); + } + } + + Future _abortCurrentSync() async { + if (_abortActiveSync case final disconnector?) { + /// Checking `disconnecter.aborted` prevents race conditions + /// where multiple calls to `disconnect` can attempt to abort + /// the controller more than once before it has finished aborting. + if (disconnector.aborted == false) { + await disconnector.abort(); + _abortActiveSync = null; + } else { + /// Wait for the abort to complete. Continue updating the sync status after completed + await disconnector.onCompletion; + } + } + } + + Future disconnect() async { + // Also wrap this in the sync mutex to ensure there's no race between us + // connecting and disconnecting. + await _activeGroup.syncConnectMutex.lock(() async { + await _abortCurrentSync(); + _subscriptionsChanged?.close(); + _subscriptionsChanged = null; + }); + + manuallyChangeSyncStatus( + SyncStatus(connected: false, lastSyncedAt: currentStatus.lastSyncedAt)); + } + + Future firstStatusMatching(bool Function(SyncStatus) predicate) async { + if (predicate(currentStatus)) { + return; + } + await for (final result in statusStream) { + if (predicate(result)) { + break; + } + } + } + + List get _subscribedStreams => [ + for (final active in _locallyActiveSubscriptions.values) + (name: active.name, parameters: active.encodedParameters) + ]; + + Future connect({ + required PowerSyncBackendConnector connector, + required ResolvedSyncOptions options, + }) async { + if (db.schema.rawTables.isNotEmpty && + options.source.syncImplementation != SyncClientImplementation.rust) { + throw UnsupportedError( + 'Raw tables are only supported by the Rust client.'); + } + + var thisConnectAborter = AbortController(); + final zone = Zone.current; + + late void Function() retryHandler; + + final subscriptionsChanged = StreamController(); + + Future connectWithSyncLock() async { + // Ensure there has not been a subsequent connect() call installing a new + // sync client. + assert(identical(_abortActiveSync, thisConnectAborter)); + assert(!thisConnectAborter.aborted); + + // ignore: invalid_use_of_protected_member + await db.connectInternal( + connector: connector, + options: options, + abort: thisConnectAborter, + initiallyActiveStreams: _subscribedStreams, + activeStreams: subscriptionsChanged.stream.map((_) { + return _subscribedStreams; + }), + // Run follow-up async tasks in the parent zone, a new one is introduced + // while we hold the lock (and async tasks won't hold the sync lock). + asyncWorkZone: zone, + ); + + thisConnectAborter.onCompletion.whenComplete(retryHandler); + } + + // If the sync encounters a failure without being aborted, retry + retryHandler = Zone.current.bindCallback(() async { + _activeGroup.syncConnectMutex.lock(() async { + // Is this still supposed to be active? (abort is only called within + // mutex) + if (!thisConnectAborter.aborted) { + // We only change _abortActiveSync after disconnecting, which resets + // the abort controller. + assert(identical(_abortActiveSync, thisConnectAborter)); + + // We need a new abort controller for this attempt + _abortActiveSync = thisConnectAborter = AbortController(); + + db.logger.warning('Sync client failed, retrying...'); + await connectWithSyncLock(); + } + }); + }); + + await _activeGroup.syncConnectMutex.lock(() async { + // Disconnect a previous sync client, if one is active. + await _abortCurrentSync(); + assert(_abortActiveSync == null); + _subscriptionsChanged = subscriptionsChanged; + + // Install the abort controller for this particular connect call, allowing + // it to be disconnected. + _abortActiveSync = thisConnectAborter; + await connectWithSyncLock(); + }); + } + + void manuallyChangeSyncStatus(SyncStatus status) { + if (status != currentStatus) { + final newStatus = SyncStatus( + connected: status.connected, + downloading: status.downloading, + uploading: status.uploading, + connecting: status.connecting, + uploadError: status.uploadError, + downloadError: status.downloadError, + priorityStatusEntries: status.priorityStatusEntries, + downloadProgress: status.downloadProgress, + // Note that currently the streaming sync implementation will never set + // hasSynced. lastSyncedAt implies that syncing has completed at some + // point (hasSynced = true). + // The previous values of hasSynced should be preserved here. + lastSyncedAt: status.lastSyncedAt ?? currentStatus.lastSyncedAt, + hasSynced: status.lastSyncedAt != null + ? true + : status.hasSynced ?? currentStatus.hasSynced, + streamSubscriptions: status.internalSubscriptions, + ); + + // If the absence of hasSynced was the only difference, the new states + // would be equal and don't require an event. So, check again. + if (newStatus != currentStatus) { + _currentStatus = newStatus; + _statusController.add(_currentStatus); + } + } + } + + _SyncStreamSubscriptionHandle _referenceStreamSubscription( + String stream, Map? parameters) { + final key = (stream, json.encode(parameters)); + _ActiveSubscription active; + + if (_locallyActiveSubscriptions[key] case final current?) { + active = current; + } else { + active = _ActiveSubscription(this, + name: stream, parameters: parameters, encodedParameters: key.$2); + _locallyActiveSubscriptions[key] = active; + _subscriptionsChanged?.add(null); + } + + return _SyncStreamSubscriptionHandle(active); + } + + void _clearSubscription(_ActiveSubscription subscription) { + assert(subscription.refcount == 0); + _locallyActiveSubscriptions + .remove((subscription.name, subscription.encodedParameters)); + _subscriptionsChanged?.add(null); + } + + Future _subscriptionsCommand(Object? command) async { + await db.writeTransaction((tx) { + return tx.execute( + 'SELECT powersync_control(?, ?)', + ['subscriptions', json.encode(command)], + ); + }); + _subscriptionsChanged?.add(null); + } + + Future subscribe({ + required String stream, + required Map? parameters, + Duration? ttl, + StreamPriority? priority, + }) async { + await _subscriptionsCommand({ + 'subscribe': { + 'stream': { + 'name': stream, + 'params': parameters, + }, + 'ttl': ttl?.inSeconds, + 'priority': priority, + }, + }); + + await _activeGroup.syncConnectMutex.lock(() async { + if (_abortActiveSync == null) { + // Since we're not connected, update the offline sync status to reflect + // the new subscription. + // With a connection, the sync client would include it in its state. + await resolveOfflineSyncStatus(); + } + }); + } + + Future unsubscribeAll({ + required String stream, + required Object? parameters, + }) async { + await _subscriptionsCommand({ + 'unsubscribe': { + 'name': stream, + 'params': parameters, + }, + }); + } + + Future resolveOfflineSyncStatus() async { + final row = await db.database.get( + 'SELECT powersync_offline_sync_status() AS r;', + ); + + final status = CoreSyncStatus.fromJson( + json.decode(row['r'] as String) as Map); + + manuallyChangeSyncStatus((MutableSyncStatus()..applyFromCore(status)) + .immutableSnapshot(setLastSynced: true)); + } + + SyncStream syncStream(String name, Map? parameters) { + return _SyncStreamImplementation(this, name, parameters); + } + + void close() { + _statusController.close(); + } +} + +final class _SyncStreamImplementation implements SyncStream { + @override + final String name; + + @override + final Map? parameters; + + final ConnectionManager _connections; + + _SyncStreamImplementation(this._connections, this.name, this.parameters); + + @override + Future subscribe({ + Duration? ttl, + StreamPriority? priority, + }) async { + await _connections.subscribe( + stream: name, + parameters: parameters, + ttl: ttl, + priority: priority, + ); + + return _connections._referenceStreamSubscription(name, parameters); + } + + @override + Future unsubscribeAll() async { + await _connections.unsubscribeAll(stream: name, parameters: parameters); + } +} + +final class _ActiveSubscription { + final ConnectionManager connections; + var refcount = 0; + + final String name; + final String encodedParameters; + final Map? parameters; + + _ActiveSubscription( + this.connections, { + required this.name, + required this.encodedParameters, + required this.parameters, + }); + + void decrementRefCount() { + refcount--; + if (refcount == 0) { + connections._clearSubscription(this); + } + } +} + +final class _SyncStreamSubscriptionHandle implements SyncStreamSubscription { + final _ActiveSubscription _source; + var _active = true; + + _SyncStreamSubscriptionHandle(this._source) { + _source.refcount++; + _finalizer.attach(this, _source, detach: this); + } + + @override + String get name => _source.name; + + @override + Map? get parameters => _source.parameters; + + @override + void unsubscribe() { + if (_active) { + _active = false; + _finalizer.detach(this); + _source.decrementRefCount(); + } + } + + @override + Future waitForFirstSync() async { + return _source.connections.firstStatusMatching((status) { + final currentProgress = status.forStream(this); + return currentProgress?.subscription.hasSynced ?? false; + }); + } + + static final Finalizer<_ActiveSubscription> _finalizer = Finalizer((sub) { + sub.connections.db.logger.warning( + 'A subscription to ${sub.name} (with parameters ${sub.parameters}) ' + 'leaked! Please ensure calling SyncStreamSubscription.unsubscribe() ' + "when you don't need a subscription anymore. For global " + 'subscriptions, consider storing them in global fields to avoid this ' + 'warning.'); + }); +} diff --git a/packages/powersync_core/lib/src/sync/instruction.dart b/packages/powersync_core/lib/src/sync/instruction.dart new file mode 100644 index 00000000..3479e281 --- /dev/null +++ b/packages/powersync_core/lib/src/sync/instruction.dart @@ -0,0 +1,163 @@ +import 'stream.dart'; +import 'sync_status.dart'; + +/// An internal instruction emitted by the sync client in the core extension in +/// response to the Dart SDK passing sync data into the extension. +sealed class Instruction { + factory Instruction.fromJson(Map json) { + return switch (json) { + {'LogLine': final logLine} => + LogLine.fromJson(logLine as Map), + {'UpdateSyncStatus': final updateStatus} => + UpdateSyncStatus.fromJson(updateStatus as Map), + {'EstablishSyncStream': final establish} => + EstablishSyncStream.fromJson(establish as Map), + {'FetchCredentials': final creds} => + FetchCredentials.fromJson(creds as Map), + {'CloseSyncStream': final closeOptions as Map} => + CloseSyncStream(closeOptions['hide_disconnect'] as bool), + {'FlushFileSystem': _} => const FlushFileSystem(), + {'DidCompleteSync': _} => const DidCompleteSync(), + _ => UnknownSyncInstruction(json) + }; + } +} + +final class LogLine implements Instruction { + final String severity; + final String line; + + LogLine({required this.severity, required this.line}); + + factory LogLine.fromJson(Map json) { + return LogLine( + severity: json['severity'] as String, + line: json['line'] as String, + ); + } +} + +final class EstablishSyncStream implements Instruction { + final Map request; + + EstablishSyncStream(this.request); + + factory EstablishSyncStream.fromJson(Map json) { + return EstablishSyncStream(json['request'] as Map); + } +} + +final class UpdateSyncStatus implements Instruction { + final CoreSyncStatus status; + + UpdateSyncStatus({required this.status}); + + factory UpdateSyncStatus.fromJson(Map json) { + return UpdateSyncStatus( + status: + CoreSyncStatus.fromJson(json['status'] as Map)); + } +} + +final class CoreSyncStatus { + final bool connected; + final bool connecting; + final List priorityStatus; + final DownloadProgress? downloading; + final List? streams; + + CoreSyncStatus({ + required this.connected, + required this.connecting, + required this.priorityStatus, + required this.downloading, + required this.streams, + }); + + factory CoreSyncStatus.fromJson(Map json) { + return CoreSyncStatus( + connected: json['connected'] as bool, + connecting: json['connecting'] as bool, + priorityStatus: [ + for (final entry in json['priority_status'] as List) + _priorityStatusFromJson(entry as Map) + ], + downloading: switch (json['downloading']) { + null => null, + final raw as Map => DownloadProgress.fromJson(raw), + }, + streams: (json['streams'] as List) + .map((e) => + CoreActiveStreamSubscription.fromJson(e as Map)) + .toList(), + ); + } + + static SyncPriorityStatus _priorityStatusFromJson(Map json) { + return ( + priority: StreamPriority(json['priority'] as int), + hasSynced: json['has_synced'] as bool?, + lastSyncedAt: switch (json['last_synced_at']) { + null => null, + final lastSyncedAt as int => + DateTime.fromMillisecondsSinceEpoch(lastSyncedAt * 1000), + }, + ); + } +} + +final class DownloadProgress { + final Map buckets; + + DownloadProgress(this.buckets); + + factory DownloadProgress.fromJson(Map line) { + final rawBuckets = line['buckets'] as Map; + + return DownloadProgress(rawBuckets.map((k, v) { + return MapEntry( + k, + _bucketProgressFromJson(v as Map), + ); + })); + } + + static BucketProgress _bucketProgressFromJson(Map json) { + return ( + priority: StreamPriority(json['priority'] as int), + atLast: json['at_last'] as int, + sinceLast: json['since_last'] as int, + targetCount: json['target_count'] as int, + ); + } +} + +final class FetchCredentials implements Instruction { + final bool didExpire; + + FetchCredentials(this.didExpire); + + factory FetchCredentials.fromJson(Map line) { + return FetchCredentials(line['did_expire'] as bool); + } +} + +final class CloseSyncStream implements Instruction { + final bool hideDisconnect; + + const CloseSyncStream(this.hideDisconnect); +} + +final class FlushFileSystem implements Instruction { + const FlushFileSystem(); +} + +final class DidCompleteSync implements Instruction { + const DidCompleteSync(); +} + +final class UnknownSyncInstruction implements Instruction { + final Map source; + + UnknownSyncInstruction(this.source); +} diff --git a/packages/powersync_core/lib/src/sync/internal_connector.dart b/packages/powersync_core/lib/src/sync/internal_connector.dart new file mode 100644 index 00000000..a18914c7 --- /dev/null +++ b/packages/powersync_core/lib/src/sync/internal_connector.dart @@ -0,0 +1,92 @@ +import 'package:meta/meta.dart'; + +import '../connector.dart'; +import '../database/powersync_database.dart'; + +/// A view over a backend connector that does not require a reference to the +/// PowerSync database. +@internal +abstract interface class InternalConnector { + /// Fetch or return cached credentials. + Future getCredentialsCached(); + + /// Ask the backend connector to fetch a new set of credentials. + /// + /// [invalidate] describes whether the current ([getCredentialsCached]) + /// credentials are already invalid, or whether this call is a pre-fetch. + /// + /// A call to [getCredentialsCached] after this future completes should return + /// the same credentials. + Future prefetchCredentials({bool invalidate = false}); + + /// Requests the connector to upload a crud batch to the backend. + Future uploadCrud(); + + const factory InternalConnector({ + required Future Function() getCredentialsCached, + required Future Function({required bool invalidate}) + prefetchCredentials, + required Future Function() uploadCrud, + }) = _CallbackConnector; + + factory InternalConnector.wrap( + PowerSyncBackendConnector connector, PowerSyncDatabase db) { + return _WrapConnector(connector, db); + } +} + +final class _WrapConnector implements InternalConnector { + final PowerSyncBackendConnector connector; + final PowerSyncDatabase database; + + _WrapConnector(this.connector, this.database); + + @override + Future getCredentialsCached() async { + return connector.getCredentialsCached(); + } + + @override + Future prefetchCredentials({bool invalidate = false}) { + if (invalidate) { + connector.invalidateCredentials(); + } + return connector.prefetchCredentials(); + } + + @override + Future uploadCrud() { + return connector.uploadData(database); + } +} + +final class _CallbackConnector implements InternalConnector { + final Future Function() _getCredentialsCached; + final Future Function({required bool invalidate}) + _prefetchCredentials; + final Future Function() _uploadCrud; + + const _CallbackConnector({ + required Future Function() getCredentialsCached, + required Future Function({required bool invalidate}) + prefetchCredentials, + required Future Function() uploadCrud, + }) : _getCredentialsCached = getCredentialsCached, + _prefetchCredentials = prefetchCredentials, + _uploadCrud = uploadCrud; + + @override + Future getCredentialsCached() { + return _getCredentialsCached(); + } + + @override + Future prefetchCredentials({bool invalidate = false}) { + return _prefetchCredentials(invalidate: invalidate); + } + + @override + Future uploadCrud() { + return _uploadCrud(); + } +} diff --git a/packages/powersync_core/lib/src/sync/mutable_sync_status.dart b/packages/powersync_core/lib/src/sync/mutable_sync_status.dart new file mode 100644 index 00000000..273cd597 --- /dev/null +++ b/packages/powersync_core/lib/src/sync/mutable_sync_status.dart @@ -0,0 +1,143 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; + +import 'instruction.dart'; +import 'stream.dart'; +import 'sync_status.dart'; +import 'bucket_storage.dart'; +import 'protocol.dart'; + +final class MutableSyncStatus { + bool connected = false; + bool connecting = false; + bool downloading = false; + bool uploading = false; + + InternalSyncDownloadProgress? downloadProgress; + List priorityStatusEntries = const []; + List? streams; + + DateTime? lastSyncedAt; + + Object? uploadError; + Object? downloadError; + + void setConnectingIfNotConnected() { + if (!connected) { + connecting = true; + } + } + + void setConnected() { + connected = true; + connecting = false; + } + + void applyDownloadError(Object error) { + connected = false; + connecting = false; + downloading = false; + downloadProgress = null; + downloadError = error; + } + + void applyCheckpointReached(Checkpoint applied) { + downloading = false; + downloadProgress = null; + downloadError = null; + final now = lastSyncedAt = DateTime.now(); + priorityStatusEntries = [ + if (applied.checksums.isNotEmpty) + ( + hasSynced: true, + lastSyncedAt: now, + priority: maxBy( + applied.checksums.map((cs) => StreamPriority(cs.priority)), + (priority) => priority, + compare: StreamPriority.comparator, + )!, + ) + ]; + } + + void applyCheckpointStarted( + Map localProgress, + Checkpoint target, + ) { + downloading = true; + downloadProgress = + InternalSyncDownloadProgress.forNewCheckpoint(localProgress, target); + } + + void applyUploadError(Object error) { + uploading = false; + uploadError = error; + } + + void applyBatchReceived(SyncDataBatch batch) { + downloading = true; + if (downloadProgress case final previousProgress?) { + downloadProgress = previousProgress.incrementDownloaded(batch); + } + } + + void applyFromCore(CoreSyncStatus status) { + connected = status.connected; + connecting = status.connecting; + downloading = status.downloading != null; + priorityStatusEntries = status.priorityStatus; + downloadProgress = switch (status.downloading) { + null => null, + final downloading => InternalSyncDownloadProgress(downloading.buckets), + }; + lastSyncedAt = status.priorityStatus + .firstWhereOrNull((s) => s.priority == StreamPriority.fullSyncPriority) + ?.lastSyncedAt; + streams = status.streams; + } + + SyncStatus immutableSnapshot({bool setLastSynced = false}) { + return SyncStatus( + connected: connected, + connecting: connecting, + downloading: downloading, + uploading: uploading, + downloadProgress: downloadProgress?.asSyncDownloadProgress, + priorityStatusEntries: UnmodifiableListView(priorityStatusEntries), + lastSyncedAt: lastSyncedAt, + hasSynced: setLastSynced ? lastSyncedAt != null : null, + uploadError: uploadError, + downloadError: downloadError, + streamSubscriptions: streams, + ); + } +} + +final class SyncStatusStateStream { + final MutableSyncStatus status = MutableSyncStatus(); + SyncStatus _lastPublishedStatus = const SyncStatus(); + + final StreamController _statusStreamController = + StreamController.broadcast(); + + Stream get statusStream => _statusStreamController.stream; + + void updateStatus(void Function(MutableSyncStatus status) change) { + change(status); + + if (_statusStreamController.isClosed) { + return; + } + + final current = status.immutableSnapshot(); + if (current != _lastPublishedStatus) { + _statusStreamController.add(current); + _lastPublishedStatus = current; + } + } + + void close() { + _statusStreamController.close(); + } +} diff --git a/packages/powersync_core/lib/src/sync/options.dart b/packages/powersync_core/lib/src/sync/options.dart new file mode 100644 index 00000000..ee8b3c63 --- /dev/null +++ b/packages/powersync_core/lib/src/sync/options.dart @@ -0,0 +1,128 @@ +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; + +/// Options that affect how the sync client connects to the sync service. +final class SyncOptions { + /// A JSON object that is passed to the sync service and forwarded to sync + /// rules. + /// + /// These [parameters](https://docs.powersync.com/usage/sync-rules/advanced-topics/client-parameters) + /// can be used in sync rules to deliver different data to different clients + /// depending on the values used in [params]. + final Map? params; + + /// A throttle to apply when listening for local database changes before + /// scheduling them for uploads. + /// + /// The throttle is applied to avoid frequent tiny writes in favor of more + /// efficient batched uploads. When set to null, PowerSync defaults to a + /// throtle duration of 10 milliseconds. + final Duration? crudThrottleTime; + + /// How long PowerSync should wait before reconnecting after an error. + /// + /// When set to null, PowerSync defaults to a delay of 5 seconds. + final Duration? retryDelay; + + /// The [SyncClientImplementation] to use. + final SyncClientImplementation syncImplementation; + + /// Whether streams that have been defined with `auto_subscribe: true` should + /// be synced when they don't have an explicit subscription. + /// + /// This is enabled by default. + final bool? includeDefaultStreams; + + const SyncOptions({ + this.crudThrottleTime, + this.retryDelay, + this.params, + this.syncImplementation = SyncClientImplementation.defaultClient, + this.includeDefaultStreams, + }); + + SyncOptions _copyWith({ + Duration? crudThrottleTime, + Duration? retryDelay, + Map? params, + }) { + return SyncOptions( + crudThrottleTime: crudThrottleTime ?? this.crudThrottleTime, + retryDelay: retryDelay, + params: params ?? this.params, + syncImplementation: syncImplementation, + includeDefaultStreams: includeDefaultStreams, + ); + } +} + +/// The PowerSync SDK offers two different implementations for receiving sync +/// lines: One handling most logic in Dart, and a newer one offloading that work +/// to the native PowerSync extension. +enum SyncClientImplementation { + /// A sync implementation that decodes and handles sync lines in Dart. + @Deprecated( + "Don't use SyncClientImplementation.dart directly, " + "use SyncClientImplementation.defaultClient instead.", + ) + dart, + + /// An experimental sync implementation that parses and handles sync lines in + /// the native PowerSync core extensions. + /// + /// This implementation can be more performant than the Dart implementation, + /// and supports receiving sync lines in a more efficient format. + /// + /// Note that this option is currently experimental. + @experimental + rust; + + /// The default sync client implementation to use. + // ignore: deprecated_member_use_from_same_package + static const defaultClient = dart; +} + +@internal +extension type ResolvedSyncOptions(SyncOptions source) { + factory ResolvedSyncOptions.resolve( + SyncOptions? source, { + Duration? crudThrottleTime, + Duration? retryDelay, + Map? params, + }) { + return ResolvedSyncOptions((source ?? SyncOptions())._copyWith( + crudThrottleTime: crudThrottleTime, + retryDelay: retryDelay, + params: params, + )); + } + + Duration get crudThrottleTime => + source.crudThrottleTime ?? const Duration(milliseconds: 10); + + Duration get retryDelay => source.retryDelay ?? const Duration(seconds: 5); + + Map get params => source.params ?? const {}; + + bool get includeDefaultStreams => source.includeDefaultStreams ?? true; + + (ResolvedSyncOptions, bool) applyFrom(SyncOptions other) { + final newOptions = SyncOptions( + crudThrottleTime: other.crudThrottleTime ?? crudThrottleTime, + retryDelay: other.retryDelay ?? retryDelay, + params: other.params ?? params, + syncImplementation: other.syncImplementation, + includeDefaultStreams: + other.includeDefaultStreams ?? includeDefaultStreams, + ); + + final didChange = !_mapEquality.equals(newOptions.params, params) || + newOptions.crudThrottleTime != crudThrottleTime || + newOptions.retryDelay != retryDelay || + newOptions.syncImplementation != source.syncImplementation || + newOptions.includeDefaultStreams != includeDefaultStreams; + return (ResolvedSyncOptions(newOptions), didChange); + } + + static const _mapEquality = MapEquality(); +} diff --git a/packages/powersync_core/lib/src/sync/protocol.dart b/packages/powersync_core/lib/src/sync/protocol.dart new file mode 100644 index 00000000..4e07334b --- /dev/null +++ b/packages/powersync_core/lib/src/sync/protocol.dart @@ -0,0 +1,395 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'bucket_storage.dart'; + +/// Messages sent from the sync service. +sealed class StreamingSyncLine { + const StreamingSyncLine(); + + /// Parses a [StreamingSyncLine] from JSON. + static StreamingSyncLine fromJson(Map line) { + if (line.containsKey('checkpoint')) { + return Checkpoint.fromJson(line['checkpoint'] as Map); + } else if (line.containsKey('checkpoint_diff')) { + return StreamingSyncCheckpointDiff.fromJson( + line['checkpoint_diff'] as Map); + } else if (line.containsKey('checkpoint_complete')) { + return StreamingSyncCheckpointComplete.fromJson( + line['checkpoint_complete'] as Map); + } else if (line.containsKey('partial_checkpoint_complete')) { + return StreamingSyncCheckpointPartiallyComplete.fromJson( + line['partial_checkpoint_complete'] as Map); + } else if (line.containsKey('data')) { + return SyncDataBatch([ + SyncBucketData.fromJson(line['data'] as Map), + ]); + } else if (line.containsKey('token_expires_in')) { + return StreamingSyncKeepalive.fromJson(line); + } else { + return UnknownSyncLine(line); + } + } + + /// A [StreamTransformer] that returns a stream emitting raw JSON objects into + /// a stream emitting [StreamingSyncLine]. + static StreamTransformer, StreamingSyncLine> reader = + StreamTransformer.fromBind((source) { + return Stream.eventTransformed(source, _StreamingSyncLineParser.new); + }); +} + +final class _StreamingSyncLineParser + implements EventSink> { + final EventSink _out; + + /// When we receive multiple `data` lines in quick succession, group them into + /// a single batch. This will make the streaming sync service insert them with + /// a single transaction, which is more efficient than inserting them + /// individually. + (SyncDataBatch, Timer)? _pendingBatch; + + _StreamingSyncLineParser(this._out); + + void _flushBatch() { + if (_pendingBatch case (final pending, final timer)?) { + timer.cancel(); + _pendingBatch = null; + _out.add(pending); + } + } + + @override + void add(Map event) { + final parsed = StreamingSyncLine.fromJson(event); + + // Buffer small batches and group them to reduce amounts of transactions + // used to store them. + if (parsed is SyncDataBatch && parsed.totalOperations <= 100) { + if (_pendingBatch case (final batch, _)?) { + // Add this line to the pending batch of data items + batch.buckets.addAll(parsed.buckets); + + if (batch.totalOperations >= 1000) { + // This is unlikely to happen since we're only buffering for a single + // event loop iteration, but make sure we're not keeping huge amonts + // of data in memory. + _flushBatch(); + } + } else { + // Insert of adding this batch directly, keep it buffered here for a + // while so that we can add new entries to it. + final timer = Timer(Duration.zero, () { + _out.add(_pendingBatch!.$1); + _pendingBatch = null; + }); + _pendingBatch = (parsed, timer); + } + } else { + _flushBatch(); + _out.add(parsed); + } + } + + @override + void addError(Object error, [StackTrace? stackTrace]) { + _flushBatch(); + _out.addError(error, stackTrace); + } + + @override + void close() { + _flushBatch(); + _out.close(); + } +} + +/// A message from the sync service that this client doesn't support. +final class UnknownSyncLine implements StreamingSyncLine { + final Map rawData; + + const UnknownSyncLine(this.rawData); +} + +/// Indicates that a checkpoint is available, along with checksums for each +/// bucket in the checkpoint. +/// +/// Note: Called `StreamingSyncCheckpoint` in sync service protocol. +final class Checkpoint extends StreamingSyncLine { + final String lastOpId; + final String? writeCheckpoint; + final List checksums; + + const Checkpoint( + {required this.lastOpId, required this.checksums, this.writeCheckpoint}); + + Checkpoint.fromJson(Map json) + : lastOpId = json['last_op_id'] as String, + writeCheckpoint = json['write_checkpoint'] as String?, + checksums = (json['buckets'] as List) + .map((b) => BucketChecksum.fromJson(b as Map)) + .toList(); + + Map toJson({int? priority}) { + return { + 'last_op_id': lastOpId, + 'write_checkpoint': writeCheckpoint, + 'buckets': checksums + .where((c) => priority == null || c.priority <= priority) + .map((c) => c.toJson()) + .toList(growable: false) + }; + } +} + +class BucketChecksum { + final String bucket; + final int priority; + final int checksum; + + /// Count is informational only + final int? count; + final String? lastOpId; + + const BucketChecksum( + {required this.bucket, + required this.priority, + required this.checksum, + this.count, + this.lastOpId}); + + BucketChecksum.fromJson(Map json) + : bucket = json['bucket'] as String, + // Use the default priority (3) as a fallback if the server doesn't send + // priorities. This value is arbitrary though, it won't get used since + // servers not sending priorities also won't send partial checkpoints. + priority = json['priority'] as int? ?? 3, + checksum = json['checksum'] as int, + count = json['count'] as int?, + lastOpId = json['last_op_id'] as String?; + + Map toJson() { + return { + 'bucket': bucket, + 'checksum': checksum, + 'priority': priority, + 'count': count, + }; + } +} + +/// A variant of [Checkpoint] that may be sent when the server has already sent +/// a [Checkpoint] message before. +/// +/// It has the same conceptual meaning as a [Checkpoint] message, but only +/// contains details about changed buckets as an optimization. +final class StreamingSyncCheckpointDiff extends StreamingSyncLine { + String lastOpId; + List updatedBuckets; + List removedBuckets; + String? writeCheckpoint; + + StreamingSyncCheckpointDiff( + this.lastOpId, this.updatedBuckets, this.removedBuckets); + + StreamingSyncCheckpointDiff.fromJson(Map json) + : lastOpId = json['last_op_id'] as String, + writeCheckpoint = json['write_checkpoint'] as String?, + updatedBuckets = (json['updated_buckets'] as List) + .map((e) => BucketChecksum.fromJson(e as Map)) + .toList(), + removedBuckets = (json['removed_buckets'] as List).cast(); +} + +/// Sent after the last [SyncBucketData] message for a checkpoint. +/// +/// Since this indicates that we may have a consistent view of the data, the +/// client may make previous [SyncBucketData] rows visible to the application +/// at this point. +final class StreamingSyncCheckpointComplete extends StreamingSyncLine { + String lastOpId; + + StreamingSyncCheckpointComplete(this.lastOpId); + + StreamingSyncCheckpointComplete.fromJson(Map json) + : lastOpId = json['last_op_id'] as String; +} + +/// Sent after all the [SyncBucketData] messages for a given priority within a +/// checkpoint have been sent. +final class StreamingSyncCheckpointPartiallyComplete extends StreamingSyncLine { + String lastOpId; + int bucketPriority; + + StreamingSyncCheckpointPartiallyComplete(this.lastOpId, this.bucketPriority); + + StreamingSyncCheckpointPartiallyComplete.fromJson(Map json) + : lastOpId = json['last_op_id'] as String, + bucketPriority = json['priority'] as int; +} + +/// Sent as a periodic ping to keep the connection alive and to notify the +/// client about the remaining lifetime of the JWT. +/// +/// When the token is nearing its expiry date, the client may ask for another +/// one and open a new sync session with that token. +final class StreamingSyncKeepalive extends StreamingSyncLine { + int tokenExpiresIn; + + StreamingSyncKeepalive(this.tokenExpiresIn); + + StreamingSyncKeepalive.fromJson(Map json) + : tokenExpiresIn = json['token_expires_in'] as int; +} + +class StreamingSyncRequest { + List buckets; + bool includeChecksum = true; + String clientId; + Map? parameters; + + StreamingSyncRequest(this.buckets, this.parameters, this.clientId); + + Map toJson() { + final Map json = { + 'buckets': buckets, + 'include_checksum': includeChecksum, + 'raw_data': true, + 'client_id': clientId + }; + + if (parameters != null) { + json['parameters'] = parameters; + } + + return json; + } +} + +class BucketRequest { + String name; + String after; + + BucketRequest(this.name, this.after); + + Map toJson() => { + 'name': name, + 'after': after, + }; +} + +/// A batch of sync operations being delivered from the sync service. +/// +/// Note that the service will always send individual [SyncBucketData] lines, +/// but we group them into [SyncDataBatch]es because writing multiple entries +/// at once improves performance. +final class SyncDataBatch extends StreamingSyncLine { + List buckets; + + int get totalOperations => + buckets.fold(0, (prev, data) => prev + data.data.length); + + SyncDataBatch(this.buckets); +} + +final class SyncBucketData { + final String bucket; + final List data; + final bool hasMore; + final String? after; + final String? nextAfter; + + const SyncBucketData( + {required this.bucket, + required this.data, + this.hasMore = false, + this.after, + this.nextAfter}); + + SyncBucketData.fromJson(Map json) + : bucket = json['bucket'] as String, + hasMore = json['has_more'] as bool? ?? false, + after = json['after'] as String?, + nextAfter = json['next_after'] as String?, + data = (json['data'] as List) + .map((e) => OplogEntry.fromJson(e as Map)) + .toList(); + + Map toJson() { + return { + 'bucket': bucket, + 'has_more': hasMore, + 'after': after, + 'next_after': nextAfter, + 'data': data + }; + } +} + +class OplogEntry { + final String opId; + + final OpType? op; + + /// rowType + rowId uniquely identifies an entry in the local database. + final String? rowType; + final String? rowId; + + /// Together with rowType and rowId, this uniquely identifies a source entry + /// per bucket in the oplog. There may be multiple source entries for a single + /// "rowType + rowId" combination. + final String? subkey; + + final String? data; + final int checksum; + + const OplogEntry( + {required this.opId, + required this.op, + this.subkey, + this.rowType, + this.rowId, + this.data, + required this.checksum}); + + OplogEntry.fromJson(Map json) + : opId = json['op_id'] as String, + op = OpType.fromJson(json['op'] as String), + rowType = json['object_type'] as String?, + rowId = json['object_id'] as String?, + checksum = json['checksum'] as int, + data = switch (json['data']) { + String data => data, + var other => jsonEncode(other), + }, + subkey = switch (json['subkey']) { + String subkey => subkey, + _ => null, + }; + + Map? get parsedData { + return switch (data) { + final data? => jsonDecode(data) as Map, + null => null, + }; + } + + /// Key to uniquely represent a source entry in a bucket. + /// This is used to supersede old entries. + /// Relevant for put and remove ops. + String get key { + return "$rowType/$rowId/$subkey"; + } + + Map toJson() { + return { + 'op_id': opId, + 'op': op?.toJson(), + 'object_type': rowType, + 'object_id': rowId, + 'checksum': checksum, + 'subkey': subkey, + 'data': data + }; + } +} diff --git a/packages/powersync_core/lib/src/sync/stream.dart b/packages/powersync_core/lib/src/sync/stream.dart new file mode 100644 index 00000000..80c447be --- /dev/null +++ b/packages/powersync_core/lib/src/sync/stream.dart @@ -0,0 +1,176 @@ +import 'package:meta/meta.dart'; + +import 'sync_status.dart'; +import '../database/powersync_database.dart'; + +/// A description of a sync stream, consisting of its [name] and the +/// [parameters] used when subscribing. +abstract interface class SyncStreamDescription { + /// The name of the stream as it appears in the stream definition for the + /// PowerSync service. + String get name; + + /// The parameters used to subscribe to the stream, if any. + /// + /// The same stream can be subscribed to multiple times with different + /// parameters. + Map? get parameters; +} + +/// Information about a subscribed sync stream. +/// +/// This includes the [SyncStreamDescription] along with information about the +/// current sync status. +abstract interface class SyncSubscriptionDescription + extends SyncStreamDescription { + /// Whether this stream is active, meaning that the subscription has been + /// acknowledged by the sync serivce. + bool get active; + + /// Whether this stream subscription is included by default, regardless of + /// whether the stream has explicitly been subscribed to or not. + /// + /// It's possible for both [isDefault] and [hasExplicitSubscription] to be + /// true at the same time - this happens when a default stream was subscribed + /// explicitly. + bool get isDefault; + + /// Whether this stream has been subscribed to explicitly. + /// + /// It's possible for both [isDefault] and [hasExplicitSubscription] to be + /// true at the same time - this happens when a default stream was subscribed + /// explicitly. + bool get hasExplicitSubscription; + + /// For sync streams that have a time-to-live, the current time at which the + /// stream would expire if not subscribed to again. + DateTime? get expiresAt; + + /// Whether this stream subscription has been synced at least once. + bool get hasSynced; + + /// If [hasSynced] is true, the last time data from this stream has been + /// synced. + DateTime? get lastSyncedAt; +} + +/// A handle to a [SyncStreamDescription] that allows subscribing to the stream. +/// +/// To obtain an instance of [SyncStream], call [PowerSyncDatabase.syncStream]. +abstract interface class SyncStream extends SyncStreamDescription { + /// Adds a subscription to this stream, requesting it to be included when + /// connecting to the sync service. + /// + /// The [priority] can be used to override the priority of this stream. + Future subscribe({ + Duration? ttl, + StreamPriority? priority, + }); + + Future unsubscribeAll(); +} + +/// A [SyncStream] that has been subscribed to. +abstract interface class SyncStreamSubscription + implements SyncStreamDescription { + /// A variant of [PowerSyncDatabase.waitForFirstSync] that is specific to + /// this stream subscription. + Future waitForFirstSync(); + + /// Removes this subscription. + /// + /// Once all [SyncStreamSubscription]s for a [SyncStream] have been + /// unsubscribed, the `ttl` for that stream starts running. When it expires + /// without subscribing again, the stream will be evicted. + void unsubscribe(); +} + +/// An `ActiveStreamSubscription` as part of the sync status in Rust. +@internal +final class CoreActiveStreamSubscription + implements SyncSubscriptionDescription { + @override + final String name; + @override + final Map? parameters; + final StreamPriority priority; + final ({int total, int downloaded}) progress; + @override + final bool active; + @override + final bool isDefault; + @override + final bool hasExplicitSubscription; + @override + final DateTime? expiresAt; + @override + final DateTime? lastSyncedAt; + + @override + bool get hasSynced => lastSyncedAt != null; + + CoreActiveStreamSubscription._({ + required this.name, + required this.parameters, + required this.priority, + required this.progress, + required this.active, + required this.isDefault, + required this.hasExplicitSubscription, + required this.expiresAt, + required this.lastSyncedAt, + }); + + factory CoreActiveStreamSubscription.fromJson(Map json) { + return CoreActiveStreamSubscription._( + name: json['name'] as String, + parameters: json['parameters'] as Map?, + priority: switch (json['priority'] as int?) { + final prio? => StreamPriority(prio), + null => StreamPriority.fullSyncPriority, + }, + progress: _progressFromJson(json['progress'] as Map), + active: json['active'] as bool, + isDefault: json['is_default'] as bool, + hasExplicitSubscription: json['has_explicit_subscription'] as bool, + expiresAt: switch (json['expires_at']) { + null => null, + final timestamp as int => + DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), + }, + lastSyncedAt: switch (json['last_synced_at']) { + null => null, + final timestamp as int => + DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), + }, + ); + } + + Map toJson() { + return { + 'name': name, + 'parameters': parameters, + 'priority': priority.priorityNumber, + 'progress': { + 'total': progress.total, + 'downloaded': progress.downloaded, + }, + 'active': active, + 'is_default': isDefault, + 'has_explicit_subscription': hasExplicitSubscription, + 'expires_at': switch (expiresAt) { + null => null, + final expiresAt => expiresAt.millisecondsSinceEpoch / 1000, + }, + 'last_synced_at': switch (lastSyncedAt) { + null => null, + final lastSyncedAt => lastSyncedAt.millisecondsSinceEpoch / 1000, + } + }; + } + + static ({int total, int downloaded}) _progressFromJson( + Map json) { + return (total: json['total'] as int, downloaded: json['downloaded'] as int); + } +} diff --git a/packages/powersync_core/lib/src/sync/stream_utils.dart b/packages/powersync_core/lib/src/sync/stream_utils.dart new file mode 100644 index 00000000..d531e783 --- /dev/null +++ b/packages/powersync_core/lib/src/sync/stream_utils.dart @@ -0,0 +1,252 @@ +import 'dart:async'; + +import 'dart:convert' as convert; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:typed_data/typed_buffers.dart'; + +import '../exceptions.dart'; + +/// Inject a broadcast stream into a normal stream. +Stream addBroadcast(Stream a, Stream broadcast) { + assert(broadcast.isBroadcast); + return mergeStreams([a, broadcast]); +} + +/// This is similar in functionality to rxdart's MergeStream. +/// The resulting stream emits values from either stream, as soon as they are +/// received. +/// +/// One difference is that if _any_ of the streams are closed, the resulting +/// stream is closed. +Stream mergeStreams(List> streams) { + final controller = StreamController(sync: true); + + List>? subscriptions; + var isClosing = false; + + controller.onListen = () { + subscriptions = streams.map((stream) { + return stream.listen( + (event) { + return controller.add(event); + }, + onError: controller.addError, + onDone: () async { + if (!isClosing) { + isClosing = true; + + try { + await cancelAll(subscriptions!); + } catch (e, s) { + controller.addError(e, s); + } finally { + controller.close(); + } + } + }, + ); + }).toList(); + }; + + controller.onCancel = () { + if (subscriptions != null && !isClosing) { + // Important: The Future must be returned here. + // Since calling cancel on one of the subscriptions may error, + // not returning the Future may result in an unhandled error. + return cancelAll(subscriptions!); + } + }; + + controller.onPause = () { + if (subscriptions != null) { + return pauseAll(subscriptions!); + } + }; + + controller.onResume = () { + if (subscriptions != null) { + return resumeAll(subscriptions!); + } + }; + + return controller.stream; +} + +extension ByteStreamToLines on Stream> { + /// Decodes this stream using UTF8 and then splits the text stream by + /// newlines. + Stream get lines { + final textInput = transform(convert.utf8.decoder); + return textInput.transform(const convert.LineSplitter()); + } + + /// Splits this stream into BSON documents without parsing them. + Stream get bsonDocuments { + return Stream.eventTransformed(this, _BsonSplittingSink.new); + } +} + +extension StreamToJson on Stream { + Stream get parseJson { + return map(convert.jsonDecode); + } +} + +void pauseAll(List> subscriptions) { + for (var sub in subscriptions) { + sub.pause(); + } +} + +void resumeAll(List> subscriptions) { + for (var sub in subscriptions) { + sub.resume(); + } +} + +Future cancelAll(List> subscriptions) async { + final futures = subscriptions.map((sub) => sub.cancel()); + await Future.wait(futures); +} + +/// A variant of [Stream.fromFuture] that makes [StreamSubscription.cancel] +/// await the original future and report errors. +/// +/// When using the regular [Stream.fromFuture], cancelling the subscription +/// before the future completes with an error could cause an handled error to +/// be reported. +/// Further, it could cause concurrency issues in the stream client because it +/// was possible for us to: +/// +/// 1. Make an HTTP request, wrapped in [Stream.fromFuture] to later expand +/// sync lines from the response. +/// 2. Receive a cancellation request, posting an event on another stream that +/// is merged into the pending HTTP stream. +/// 3. Act on the cancellation request by cancelling the merged subscription. +/// 4. As a clean-up action, close the HTTP client (while the request is still +/// being resolved). +/// +/// Running step 4 and 1 concurrently is really bad, so we delay the +/// cancellation until step 1 has completed. +/// +/// As a further discussion, note that throwing in [StreamSubscription.cancel] +/// is also not exactly a good practice. However, it is the only way to properly +/// delay cancelling streams here, since the [future] itself is a critical +/// operation that must complete first here. +Stream streamFromFutureAwaitInCancellation(Future future) { + final controller = StreamController(sync: true); + var cancelled = false; + + final handledFuture = future.then((value) { + controller + ..add(value) + ..close(); + }, onError: (Object error, StackTrace trace) { + if (cancelled) { + // Make handledFuture complete with the error, so that controller.cancel + // throws (instead of the error being unhandled). + throw error; + } else { + controller + ..addError(error, trace) + ..close(); + } + }); + + controller.onCancel = () async { + cancelled = true; + await handledFuture; + }; + + return controller.stream; +} + +/// An [EventSink] that takes raw bytes as inputs, buffers them internally by +/// reading a 4-byte length prefix for each message and then emits them as +/// chunks. +final class _BsonSplittingSink implements EventSink> { + final EventSink _downstream; + + final length = ByteData(4); + int remainingBytes = 4; + + Uint8Buffer? pendingBuffer; + + _BsonSplittingSink(this._downstream); + + @override + void add(List data) { + var i = 0; + while (i < data.length) { + final availableInData = data.length - i; + + if (pendingBuffer case final pending?) { + // We're in the middle of reading a document + final bytesToRead = min(availableInData, remainingBytes); + pending.addAll(data, i, i + bytesToRead); + i += bytesToRead; + remainingBytes -= bytesToRead; + assert(remainingBytes >= 0); + + if (remainingBytes == 0) { + _downstream.add(pending.buffer + .asUint8List(pending.offsetInBytes, pending.lengthInBytes)); + + // Prepare reading another document, starting with its length + pendingBuffer = null; + remainingBytes = 4; + } + } else { + final bytesToRead = min(availableInData, remainingBytes); + final lengthAsUint8List = length.buffer.asUint8List(); + + lengthAsUint8List.setRange( + 4 - remainingBytes, + 4 - remainingBytes + bytesToRead, + data, + i, + ); + i += bytesToRead; + remainingBytes -= bytesToRead; + assert(remainingBytes >= 0); + + if (remainingBytes == 0) { + // Transition from reading length header to reading document. + // Subtracting 4 because the length of the header is included in the + // length. + remainingBytes = length.getInt32(0, Endian.little) - 4; + if (remainingBytes < 5) { + _downstream.addError( + PowerSyncProtocolException( + 'Invalid length for bson: $remainingBytes'), + StackTrace.current, + ); + } + + pendingBuffer = Uint8Buffer()..addAll(lengthAsUint8List); + } + } + } + + assert(i == data.length); + } + + @override + void addError(Object error, [StackTrace? stackTrace]) { + _downstream.addError(error, stackTrace); + } + + @override + void close() { + if (pendingBuffer != null || remainingBytes != 4) { + _downstream.addError( + PowerSyncProtocolException('Pending data when stream was closed'), + StackTrace.current, + ); + } + + _downstream.close(); + } +} diff --git a/packages/powersync_core/lib/src/sync/streaming_sync.dart b/packages/powersync_core/lib/src/sync/streaming_sync.dart new file mode 100644 index 00000000..60deef12 --- /dev/null +++ b/packages/powersync_core/lib/src/sync/streaming_sync.dart @@ -0,0 +1,834 @@ +import 'dart:async'; +import 'dart:convert' as convert; +import 'dart:typed_data'; + +import 'package:http/http.dart' as http; +import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; +import 'package:powersync_core/src/abort_controller.dart'; +import 'package:powersync_core/src/exceptions.dart'; +import 'package:powersync_core/src/log_internal.dart'; +import 'package:powersync_core/src/sync/options.dart'; +import 'package:powersync_core/src/user_agent/user_agent.dart'; +import 'package:sqlite_async/mutex.dart'; + +import 'bucket_storage.dart'; +import '../crud.dart'; +import 'instruction.dart'; +import 'internal_connector.dart'; +import 'mutable_sync_status.dart'; +import 'stream_utils.dart'; +import 'sync_status.dart'; +import 'protocol.dart'; + +typedef SubscribedStream = ({String name, String parameters}); + +abstract interface class StreamingSync { + Stream get statusStream; + + Future streamingSync(); + + /// Close any active streams. + Future abort(); + + void updateSubscriptions(List streams); +} + +@internal +class StreamingSyncImplementation implements StreamingSync { + final String schemaJson; + final BucketStorage adapter; + final InternalConnector connector; + final ResolvedSyncOptions options; + List _activeSubscriptions; + + final Logger logger; + + final Stream crudUpdateTriggerStream; + + // An internal controller which is used to trigger CRUD uploads internally + // e.g. when reconnecting. + // This is only a broadcast controller since the `crudLoop` method is public + // and could potentially be called multiple times externally. + final StreamController _internalCrudTriggerController = + StreamController.broadcast(); + + final http.Client _client; + + final SyncStatusStateStream _state = SyncStatusStateStream(); + + AbortController? _abort; + + final Mutex syncMutex, crudMutex; + Completer? _activeCrudUpload; + final StreamController _nonLineSyncEvents = + StreamController.broadcast(); + + final Map _userAgentHeaders; + String? clientId; + + StreamingSyncImplementation({ + required this.schemaJson, + required this.adapter, + required this.connector, + required this.crudUpdateTriggerStream, + required this.options, + required http.Client client, + List activeSubscriptions = const [], + Mutex? syncMutex, + Mutex? crudMutex, + Logger? logger, + + /// A unique identifier for this streaming sync implementation + /// A good value is typically the DB file path which it will mutate when syncing. + String? identifier = "unknown", + }) : _client = client, + syncMutex = syncMutex ?? Mutex(identifier: "sync-$identifier"), + crudMutex = crudMutex ?? Mutex(identifier: "crud-$identifier"), + _userAgentHeaders = userAgentHeaders(), + logger = logger ?? isolateLogger, + _activeSubscriptions = activeSubscriptions; + + Duration get _retryDelay => options.retryDelay; + + @override + Stream get statusStream => _state.statusStream; + + @override + Future abort() async { + // If streamingSync() hasn't been called yet, _abort will be null. + if (_abort case final abort?) { + final future = abort.abort(); + _internalCrudTriggerController.close(); + + // If a sync iteration is active, the control flow to abort is: + // + // 1. We close the non-line sync event stream here. + // 2. This emits a done event. + // 3. `addBroadcastStream` will cancel all source subscriptions in + // response to that, and then emit a done event too. If there is an + // error while cancelling the stream, it's forwarded by emitting an + // error before closing. + // 4. We break out of the sync loop (either due to an error or because + // all resources have been closed correctly). + // 5. `streamingSync` completes the abort controller, which we await + // here. + await _nonLineSyncEvents.close(); + + // Wait for the abort to complete, which also guarantees that no requests + // are pending. + await Future.wait([ + future, + if (_activeCrudUpload case final activeUpload?) activeUpload.future, + ]); + + _client.close(); + _state.close(); + } + } + + bool get aborted { + return _abort?.aborted ?? false; + } + + @override + void updateSubscriptions(List streams) { + _activeSubscriptions = streams; + if (_nonLineSyncEvents.hasListener) { + _nonLineSyncEvents.add(HandleChangedSubscriptions(streams)); + } + } + + @override + Future streamingSync() async { + try { + assert(_abort == null); + _abort = AbortController(); + clientId = await adapter.getClientId(); + _crudLoop(); + var invalidCredentials = false; + while (!aborted) { + _state.updateStatus((s) => s.setConnectingIfNotConnected()); + try { + if (invalidCredentials) { + // This may error. In that case it will be retried again on the next + // iteration. + await connector.prefetchCredentials(); + invalidCredentials = false; + } + // Protect sync iterations with exclusivity (if a valid Mutex is provided) + await syncMutex.lock(() { + switch (options.source.syncImplementation) { + // ignore: deprecated_member_use_from_same_package + case SyncClientImplementation.dart: + return _dartStreamingSyncIteration(); + case SyncClientImplementation.rust: + return _rustStreamingSyncIteration(); + } + }, timeout: _retryDelay); + } catch (e, stacktrace) { + if (aborted && e is http.ClientException) { + // Explicit abort requested - ignore. Example error: + // ClientException: Connection closed while receiving data, uri=http://localhost:8080/sync/stream + return; + } + final message = _syncErrorMessage(e); + logger.warning('Sync error: $message', e, stacktrace); + invalidCredentials = true; + + _state.updateStatus((s) => s.applyDownloadError(e)); + + // On error, wait a little before retrying + // When aborting, don't wait + await _delayRetry(); + } + } + } finally { + _abort!.completeAbort(); + } + } + + Future _crudLoop() async { + await _uploadAllCrud(); + + // Trigger a CRUD upload whenever the upstream trigger fires + // as-well-as whenever the sync stream reconnects. + // This has the potential (in rare cases) to affect the crudThrottleTime, + // but it should not result in excessive uploads since the + // sync reconnects are also throttled. + // The stream here is closed on abort. + await for (var _ in mergeStreams( + [crudUpdateTriggerStream, _internalCrudTriggerController.stream])) { + await _uploadAllCrud(); + } + } + + Future _uploadAllCrud() { + assert(_activeCrudUpload == null); + final completer = _activeCrudUpload = Completer(); + return crudMutex.lock(() async { + // Keep track of the first item in the CRUD queue for the last `uploadCrud` iteration. + CrudEntry? checkedCrudItem; + + while (true) { + try { + // It's possible that an abort or disconnect operation could + // be followed by a `close` operation. The close would cause these + // operations, which use the DB, to throw an exception. Breaking the loop + // here prevents unnecessary potential (caught) exceptions. + if (aborted) { + break; + } + // This is the first item in the FIFO CRUD queue. + CrudEntry? nextCrudItem = await adapter.nextCrudItem(); + if (nextCrudItem != null) { + _state.updateStatus((s) => s.uploading = true); + if (nextCrudItem.clientId == checkedCrudItem?.clientId) { + // This will force a higher log level than exceptions which are caught here. + logger.warning( + """Potentially previously uploaded CRUD entries are still present in the upload queue. + Make sure to handle uploads and complete CRUD transactions or batches by calling and awaiting their [.complete()] method. + The next upload iteration will be delayed."""); + throw Exception( + 'Delaying due to previously encountered CRUD item.'); + } + + checkedCrudItem = nextCrudItem; + await connector.uploadCrud(); + _state.updateStatus((s) => s.uploadError = null); + } else { + // Uploading is completed + await adapter.updateLocalTarget(() => getWriteCheckpoint()); + break; + } + } catch (e, stacktrace) { + checkedCrudItem = null; + logger.warning('Data upload error', e, stacktrace); + _state.updateStatus((s) => s.applyUploadError(e)); + await _delayRetry(); + + if (!_state.status.connected) { + // Exit the upload loop if the sync stream is no longer connected + break; + } + logger.warning( + "Caught exception when uploading. Upload will retry after a delay", + e, + stacktrace); + } finally { + _state.updateStatus((s) => s.uploading = false); + } + } + }, timeout: _retryDelay).whenComplete(() { + if (!aborted) { + _nonLineSyncEvents.add(const UploadCompleted()); + } + + assert(identical(_activeCrudUpload, completer)); + _activeCrudUpload = null; + completer.complete(); + }); + } + + Future getWriteCheckpoint() async { + final credentials = await connector.getCredentialsCached(); + if (credentials == null) { + throw CredentialsException("Not logged in"); + } + final uri = + credentials.endpointUri('write-checkpoint2.json?client_id=$clientId'); + + Map headers = { + 'Content-Type': 'application/json', + 'Authorization': "Token ${credentials.token}", + ..._userAgentHeaders + }; + + final response = await _client.get(uri, headers: headers); + if (response.statusCode == 401) { + await connector.prefetchCredentials(); + } + if (response.statusCode != 200) { + throw SyncResponseException.fromResponse(response); + } + + final body = convert.jsonDecode(response.body); + return body['data']['write_checkpoint'] as String; + } + + void _updateStatusForPriority(SyncPriorityStatus completed) { + _state.updateStatus((s) { + // All status entries with a higher priority can be deleted since this + // partial sync includes them. + s.priorityStatusEntries = [ + for (final entry in s.priorityStatusEntries) + if (entry.priority < completed.priority) entry, + completed + ]; + }); + } + + Future _rustStreamingSyncIteration() async { + logger.info('Starting Rust sync iteration'); + final response = await _ActiveRustStreamingIteration(this).syncIteration(); + logger.info( + 'Ending Rust sync iteration. Immediate restart: ${response.immediateRestart}'); + // Note: With the current loop in streamingSync(), any return value that + // isn't an exception triggers an immediate restart. + } + + Future<(List, Map)> + _collectLocalBucketState() async { + final bucketEntries = await adapter.getBucketStates(); + + final initialRequests = [ + for (final entry in bucketEntries) BucketRequest(entry.bucket, entry.opId) + ]; + final localDescriptions = { + for (final entry in bucketEntries) entry.bucket: null + }; + + return (initialRequests, localDescriptions); + } + + Future _dartStreamingSyncIteration() async { + var (bucketRequests, bucketMap) = await _collectLocalBucketState(); + if (aborted) { + return; + } + + Checkpoint? targetCheckpoint; + + var requestStream = _streamingSyncRequest( + StreamingSyncRequest(bucketRequests, options.params, clientId!)) + .map(ReceivedLine.new); + + var merged = addBroadcast(requestStream, _nonLineSyncEvents.stream); + + Future? credentialsInvalidation; + bool shouldStopIteration = false; + + // Trigger a CRUD upload on reconnect + _internalCrudTriggerController.add(null); + + Future handleLine(StreamingSyncLine line) async { + switch (line) { + case Checkpoint(): + targetCheckpoint = line; + final Set bucketsToDelete = {...bucketMap.keys}; + final Map newBuckets = {}; + for (final checksum in line.checksums) { + newBuckets[checksum.bucket] = ( + name: checksum.bucket, + priority: checksum.priority, + ); + bucketsToDelete.remove(checksum.bucket); + } + bucketMap = newBuckets; + await adapter.removeBuckets([...bucketsToDelete]); + final initialProgress = await adapter.getBucketOperationProgress(); + _state.updateStatus( + (s) => s.applyCheckpointStarted(initialProgress, line)); + case StreamingSyncCheckpointComplete(): + final result = await _applyCheckpoint(targetCheckpoint!, _abort); + if (result.abort) { + shouldStopIteration = true; + return; + } + case StreamingSyncCheckpointPartiallyComplete(:final bucketPriority): + final result = await adapter.syncLocalDatabase(targetCheckpoint!, + forPriority: bucketPriority); + if (!result.checkpointValid) { + // This means checksums failed. Start again with a new checkpoint. + // TODO: better back-off + // await new Promise((resolve) => setTimeout(resolve, 50)); + shouldStopIteration = true; + return; + } else if (!result.ready) { + // If we have pending uploads, we can't complete new checkpoints + // outside of priority 0. We'll resolve this for a complete + // checkpoint later. + } else { + _updateStatusForPriority(( + priority: StreamPriority(bucketPriority), + lastSyncedAt: DateTime.now(), + hasSynced: true, + )); + } + case StreamingSyncCheckpointDiff(): + // TODO: It may be faster to just keep track of the diff, instead of + // the entire checkpoint + if (targetCheckpoint == null) { + throw PowerSyncProtocolException( + 'Checkpoint diff without previous checkpoint'); + } + final diff = line; + final Map newBuckets = {}; + for (var checksum in targetCheckpoint!.checksums) { + newBuckets[checksum.bucket] = checksum; + } + for (var checksum in diff.updatedBuckets) { + newBuckets[checksum.bucket] = checksum; + } + for (var bucket in diff.removedBuckets) { + newBuckets.remove(bucket); + } + + final newCheckpoint = Checkpoint( + lastOpId: diff.lastOpId, + checksums: [...newBuckets.values], + writeCheckpoint: diff.writeCheckpoint); + targetCheckpoint = newCheckpoint; + final initialProgress = await adapter.getBucketOperationProgress(); + _state.updateStatus( + (s) => s.applyCheckpointStarted(initialProgress, newCheckpoint)); + + bucketMap = newBuckets.map((name, checksum) => + MapEntry(name, (name: name, priority: checksum.priority))); + await adapter.removeBuckets(diff.removedBuckets); + adapter.setTargetCheckpoint(targetCheckpoint!); + case SyncDataBatch(): + // TODO: This increments the counters before actually saving sync + // data. Might be fine though? + _state.updateStatus((s) => s.applyBatchReceived(line)); + await adapter.saveSyncData(line); + case StreamingSyncKeepalive(:final tokenExpiresIn): + if (tokenExpiresIn == 0) { + // Token expired already - stop the connection immediately + connector.prefetchCredentials(invalidate: true).ignore(); + shouldStopIteration = true; + break; + } else if (tokenExpiresIn <= 30) { + // Token expires soon - refresh it in the background + credentialsInvalidation ??= + connector.prefetchCredentials().then((_) { + // Token has been refreshed - we should restart the connection. + shouldStopIteration = true; + // trigger next loop iteration ASAP, don't wait for another + // message from the server. + if (!aborted) { + _nonLineSyncEvents.add(TokenRefreshComplete()); + } + }, onError: (_) { + // Token refresh failed - retry on next keepalive. + credentialsInvalidation = null; + }); + } + case UnknownSyncLine(:final rawData): + logger.fine('Unknown sync line: $rawData'); + } + } + + await for (var line in merged) { + if (aborted || shouldStopIteration) { + break; + } + + switch (line) { + case ReceivedLine(:final line): + _state.updateStatus((s) => s.setConnected()); + await handleLine(line as StreamingSyncLine); + case UploadCompleted(): + case HandleChangedSubscriptions(): + // Only relevant for the Rust sync implementation. + break; + case AbortCurrentIteration(): + case TokenRefreshComplete(): + // We have a new token, so stop the iteration. + shouldStopIteration = true; + } + + if (shouldStopIteration) { + // Stop this connection, so that a new one will be started + break; + } + } + } + + Future<({bool abort, bool didApply})> _applyCheckpoint( + Checkpoint targetCheckpoint, AbortController? abortController) async { + var result = await adapter.syncLocalDatabase(targetCheckpoint); + final pendingUpload = _activeCrudUpload; + + if (!result.checkpointValid) { + // This means checksums failed. Start again with a new checkpoint. + // TODO: better back-off + // await new Promise((resolve) => setTimeout(resolve, 50)); + return const (abort: true, didApply: false); + } else if (!result.ready && pendingUpload != null) { + // We have pending entries in the local upload queue or are waiting to + // confirm a write checkpoint, which prevented this checkpoint from + // applying. Wait for that to complete and try again. + logger.fine('Could not apply checkpoint due to local data. ' + 'Waiting for in-progress upload before retrying...'); + await Future.any([ + pendingUpload.future, + if (abortController case final controller?) controller.onAbort, + ]); + + if (abortController?.aborted == true) { + return const (abort: true, didApply: false); + } + + // Try again now that uploads have completed. + result = await adapter.syncLocalDatabase(targetCheckpoint); + } + + if (result.checkpointValid && result.ready) { + logger.fine('validated checkpoint: $targetCheckpoint'); + + _state.updateStatus((s) => s.applyCheckpointReached(targetCheckpoint)); + + return const (abort: false, didApply: true); + } else { + logger.fine( + 'Could not apply checkpoint. Waiting for next sync complete line'); + return const (abort: false, didApply: false); + } + } + + Future _postStreamRequest( + Object? data, bool acceptBson, + {Future? onAbort}) async { + const ndJson = 'application/x-ndjson'; + const bson = 'application/vnd.powersync.bson-stream'; + + final credentials = await connector.getCredentialsCached(); + if (credentials == null) { + throw CredentialsException('Not logged in'); + } + final uri = credentials.endpointUri('sync/stream'); + + final request = http.AbortableRequest('POST', uri, + abortTrigger: onAbort ?? _abort!.onAbort); + request.headers['Content-Type'] = 'application/json'; + request.headers['Authorization'] = "Token ${credentials.token}"; + request.headers['Accept'] = + acceptBson ? '$bson;q=0.9,$ndJson;q=0.8' : ndJson; + request.headers.addAll(_userAgentHeaders); + + request.body = convert.jsonEncode(data); + + final res = await _client.send(request); + if (aborted) { + return null; + } + + if (res.statusCode == 401) { + await connector.prefetchCredentials(invalidate: true); + } + if (res.statusCode != 200) { + throw await SyncResponseException.fromStreamedResponse(res); + } + + return res; + } + + Stream _streamingSyncRequest(StreamingSyncRequest data) { + return streamFromFutureAwaitInCancellation(_postStreamRequest(data, false)) + .asyncExpand((response) { + return response?.stream.lines.parseJson + .cast>() + .transform(StreamingSyncLine.reader); + }); + } + + /// Delays the standard `retryDelay` Duration, but exits early if + /// an abort has been requested. + Future _delayRetry() async { + await Future.any([Future.delayed(_retryDelay), _abort!.onAbort]); + } +} + +/// Attempt to give a basic summary of the error for cases where the full error +/// is not logged. +String _syncErrorMessage(Object? error) { + if (error == null) { + return 'Unknown'; + } else if (error is http.ClientException) { + return 'Sync service error'; + } else if (error is SyncResponseException) { + if (error.statusCode == 401) { + return 'Authorization error'; + } else { + return 'Sync service error'; + } + } else if (error is ArgumentError || error is FormatException) { + return 'Configuration error'; + } else if (error is CredentialsException) { + return 'Credentials error'; + } else if (error is PowerSyncProtocolException) { + return 'Protocol error'; + } else { + return '${error.runtimeType}: $error'; + } +} + +typedef BucketDescription = ({ + String name, + int priority, +}); + +final class _ActiveRustStreamingIteration { + final StreamingSyncImplementation sync; + + var _isActive = true; + var _hadSyncLine = false; + + StreamSubscription? _completedUploads; + final Completer _completedStream = Completer(); + + _ActiveRustStreamingIteration(this.sync); + + List _encodeSubscriptions(List subscriptions) { + return sync._activeSubscriptions + .map((s) => + {'name': s.name, 'params': convert.json.decode(s.parameters)}) + .toList(); + } + + Future syncIteration() async { + try { + await _control( + 'start', + convert.json.encode({ + 'parameters': sync.options.params, + 'schema': convert.json.decode(sync.schemaJson), + 'include_defaults': sync.options.includeDefaultStreams, + 'active_streams': _encodeSubscriptions(sync._activeSubscriptions), + }), + ); + assert(_completedStream.isCompleted, 'Should have started streaming'); + return await _completedStream.future; + } finally { + _isActive = false; + _completedUploads?.cancel(); + await _stop(); + } + } + + Stream _receiveLines(Object? data, + {required Future onAbort}) { + return streamFromFutureAwaitInCancellation( + sync._postStreamRequest(data, true, onAbort: onAbort)) + .asyncExpand((response) { + if (response == null) { + return null; + } else { + final contentType = response.headers['content-type']; + final isBson = contentType == 'application/vnd.powersync.bson-stream'; + + return isBson ? response.stream.bsonDocuments : response.stream.lines; + } + }).map(ReceivedLine.new); + } + + Future _handleLines( + EstablishSyncStream request) async { + // This is a workaround for https://github.com/dart-lang/http/issues/1820: + // When cancelling the stream subscription of an HTTP response with the + // fetch-based client implementation, cancelling the subscription is delayed + // until the next chunk (typically a token_expires_in message in our case). + // So, before cancelling, we complete an abort controller for the request to + // speed things up. This is not an issue in most cases because the abort + // controller on this stream would be completed when disconnecting. But + // when switching sync streams, that's not the case and we need a second + // abort controller for the inner iteration. + final innerAbort = Completer.sync(); + final events = addBroadcast( + _receiveLines( + request.request, + onAbort: Future.any([ + sync._abort!.onAbort, + innerAbort.future, + ]), + ), + sync._nonLineSyncEvents.stream, + ); + + var needsImmediateRestart = false; + loop: + try { + await for (final event in events) { + if (!_isActive || sync.aborted) { + innerAbort.complete(); + break; + } + + switch (event) { + case ReceivedLine(line: final Uint8List line): + _triggerCrudUploadOnFirstLine(); + await _control('line_binary', line); + case ReceivedLine(line: final line as String): + _triggerCrudUploadOnFirstLine(); + await _control('line_text', line); + case UploadCompleted(): + await _control('completed_upload'); + case AbortCurrentIteration(:final hideDisconnectState): + innerAbort.complete(); + needsImmediateRestart = hideDisconnectState; + break loop; + case TokenRefreshComplete(): + await _control('refreshed_token'); + case HandleChangedSubscriptions(:final currentSubscriptions): + await _control( + 'update_subscriptions', + convert.json + .encode(_encodeSubscriptions(currentSubscriptions))); + } + } + } on http.RequestAbortedException { + // Unlike a regular cancellation, cancelling via the abort controller + // emits an error. We did mean to just cancel the stream, so we can + // safely ignore that. + if (innerAbort.isCompleted) { + // ignore + } else { + rethrow; + } + } + + return (immediateRestart: needsImmediateRestart); + } + + /// Triggers a local CRUD upload when the first sync line has been received. + /// + /// This allows uploading local changes that have been made while offline or + /// disconnected. + void _triggerCrudUploadOnFirstLine() { + if (!_hadSyncLine) { + sync._internalCrudTriggerController.add(null); + _hadSyncLine = true; + } + } + + Future _stop() { + return _control('stop'); + } + + Future _control(String operation, [Object? payload]) async { + final rawResponse = await sync.adapter.control(operation, payload); + final instructions = convert.json.decode(rawResponse) as List; + + for (final instruction in instructions) { + await _handleInstruction( + Instruction.fromJson(instruction as Map)); + } + } + + Future _handleInstruction(Instruction instruction) async { + switch (instruction) { + case LogLine(:final severity, :final line): + sync.logger.log( + switch (severity) { + 'DEBUG' => Level.FINE, + 'INFO' => Level.INFO, + _ => Level.WARNING, + }, + line); + case EstablishSyncStream(): + _completedStream.complete(_handleLines(instruction)); + case UpdateSyncStatus(:final status): + sync._state.updateStatus((m) => m.applyFromCore(status)); + case FetchCredentials(:final didExpire): + if (didExpire) { + await sync.connector.prefetchCredentials(invalidate: true); + } else { + sync.connector.prefetchCredentials().then((_) { + if (_isActive && !sync.aborted) { + sync._nonLineSyncEvents.add(const TokenRefreshComplete()); + } + }, onError: (Object e, StackTrace s) { + sync.logger.warning('Could not prefetch credentials', e, s); + }); + } + case CloseSyncStream(:final hideDisconnect): + if (!sync.aborted) { + _isActive = false; + sync._nonLineSyncEvents + .add(AbortCurrentIteration(hideDisconnectState: hideDisconnect)); + } + case FlushFileSystem(): + await sync.adapter.flushFileSystem(); + case DidCompleteSync(): + sync._state.updateStatus((m) => m.downloadError = null); + case UnknownSyncInstruction(:final source): + sync.logger.warning('Unknown instruction: $source'); + } + } +} + +typedef RustSyncIterationResult = ({bool immediateRestart}); + +sealed class SyncEvent {} + +final class ReceivedLine implements SyncEvent { + final Object /* String|Uint8List|StreamingSyncLine */ line; + + const ReceivedLine(this.line); +} + +final class UploadCompleted implements SyncEvent { + const UploadCompleted(); +} + +final class TokenRefreshComplete implements SyncEvent { + const TokenRefreshComplete(); +} + +final class AbortCurrentIteration implements SyncEvent { + /// Whether we should immediately disconnect and hide the `disconnected` + /// state. + /// + /// This is used when we're changing subscription, to hide the brief downtime + /// we have while reconnecting. + final bool hideDisconnectState; + + const AbortCurrentIteration({this.hideDisconnectState = false}); +} + +final class HandleChangedSubscriptions implements SyncEvent { + final List currentSubscriptions; + + HandleChangedSubscriptions(this.currentSubscriptions); +} diff --git a/packages/powersync_core/lib/src/sync/sync_status.dart b/packages/powersync_core/lib/src/sync/sync_status.dart new file mode 100644 index 00000000..61ae7c5f --- /dev/null +++ b/packages/powersync_core/lib/src/sync/sync_status.dart @@ -0,0 +1,476 @@ +import 'dart:math'; + +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; + +import 'bucket_storage.dart'; +import 'protocol.dart'; +import 'stream.dart'; + +final class SyncStatus { + /// true if currently connected. + /// + /// This means the PowerSync connection is ready to download, and + /// [PowerSyncBackendConnector.uploadData] may be called for any local changes. + final bool connected; + + /// true if the PowerSync connection is busy connecting. + /// + /// During this stage, [PowerSyncBackendConnector.uploadData] may already be called, + /// called, and [uploading] may be true. + final bool connecting; + + /// true if actively downloading changes. + /// + /// This is only true when [connected] is also true. + final bool downloading; + + /// A realtime progress report on how many operations have been downloaded and + /// how many are necessary in total to complete the next sync iteration. + /// + /// This field is only set when [downloading] is also true. + final SyncDownloadProgress? downloadProgress; + + /// true if uploading changes + final bool uploading; + + /// Time that a last sync has fully completed, if any. + /// + /// This is null while loading the database. + final DateTime? lastSyncedAt; + + /// Indicates whether there has been at least one full sync, if any. + /// Is null when unknown, for example when state is still being loaded from the database. + final bool? hasSynced; + + /// Error during uploading. + /// + /// Cleared on the next successful upload. + final Object? uploadError; + + /// Error during downloading (including connecting). + /// + /// Cleared on the next successful data download. + final Object? downloadError; + + final List priorityStatusEntries; + + final List? _internalSubscriptions; + + @internal + const SyncStatus({ + this.connected = false, + this.connecting = false, + this.lastSyncedAt, + this.hasSynced, + this.downloadProgress, + this.downloading = false, + this.uploading = false, + this.downloadError, + this.uploadError, + this.priorityStatusEntries = const [], + List? streamSubscriptions, + }) : _internalSubscriptions = streamSubscriptions; + + @override + bool operator ==(Object other) { + return (other is SyncStatus && + other.connected == connected && + other.downloading == downloading && + other.uploading == uploading && + other.connecting == connecting && + other.downloadError == downloadError && + other.uploadError == uploadError && + other.lastSyncedAt == lastSyncedAt && + other.hasSynced == hasSynced && + _listEquality.equals( + other.priorityStatusEntries, priorityStatusEntries) && + _listEquality.equals( + other._internalSubscriptions, _internalSubscriptions) && + other.downloadProgress == downloadProgress); + } + + // Deprecated because it can't set fields back to null + @Deprecated('Should not be used in user code') + SyncStatus copyWith({ + bool? connected, + bool? downloading, + bool? uploading, + bool? connecting, + Object? uploadError, + Object? downloadError, + DateTime? lastSyncedAt, + bool? hasSynced, + List? priorityStatusEntries, + }) { + return SyncStatus( + connected: connected ?? this.connected, + downloading: downloading ?? this.downloading, + uploading: uploading ?? this.uploading, + connecting: connecting ?? this.connecting, + uploadError: uploadError ?? this.uploadError, + downloadError: downloadError ?? this.downloadError, + lastSyncedAt: lastSyncedAt ?? this.lastSyncedAt, + hasSynced: hasSynced ?? this.hasSynced, + priorityStatusEntries: + priorityStatusEntries ?? this.priorityStatusEntries, + ); + } + + /// All sync streams currently being tracked in the database. + /// + /// This returns null when the database is currently being opened and we + /// don't have reliable information about all included streams yet. + Iterable? get syncStreams { + return _internalSubscriptions?.map((subscription) { + return SyncStreamStatus._(subscription, downloadProgress); + }); + } + + /// Get the current [downloadError] or [uploadError]. + Object? get anyError { + return downloadError ?? uploadError; + } + + /// Returns information for [lastSyncedAt] and [hasSynced] information at a + /// partial sync priority, or `null` if the status for that priority is + /// unknown. + /// + /// The information returned may be more generic than requested. For instance, + /// a fully-completed sync cycle (as expressed by [lastSyncedAt]) necessarily + /// includes all buckets across all priorities. So, if no further partial + /// checkpoints have been received since that complete sync, + /// [statusForPriority] may return information for that complete sync. + /// Similarly, requesting the sync status for priority `1` may return + /// information extracted from the lower priority `2` since each partial sync + /// in priority `2` necessarily includes a consistent view over data in + /// priority `1`. + SyncPriorityStatus statusForPriority(StreamPriority priority) { + assert(priorityStatusEntries.isSortedByCompare( + (e) => e.priority, StreamPriority.comparator)); + + for (final known in priorityStatusEntries) { + // Lower-priority buckets are synchronized after higher-priority buckets, + // and since priorityStatusEntries is sorted we look for the first entry + // that doesn't have a higher priority. + if (known.priority <= priority) { + return known; + } + } + + // If we have a complete sync, that necessarily includes all priorities. + return ( + priority: priority, + hasSynced: hasSynced, + lastSyncedAt: lastSyncedAt + ); + } + + /// If the [stream] appears in [syncStreams], returns the current status for + /// that stream. + SyncStreamStatus? forStream(SyncStreamDescription stream) { + final raw = _internalSubscriptions?.firstWhereOrNull( + (e) => + e.name == stream.name && + _mapEquality.equals(e.parameters, stream.parameters), + ); + + if (raw == null) { + return null; + } + return SyncStreamStatus._(raw, downloadProgress); + } + + @override + int get hashCode { + return Object.hash( + connected, + downloading, + uploading, + connecting, + uploadError, + downloadError, + lastSyncedAt, + _listEquality.hash(priorityStatusEntries), + downloadProgress, + _listEquality.hash(_internalSubscriptions), + ); + } + + @override + String toString() { + return "SyncStatus"; + } + + static const _listEquality = ListEquality(); + static const _mapEquality = MapEquality(); +} + +@internal +extension InternalSyncStatusAccess on SyncStatus { + List? get internalSubscriptions => + _internalSubscriptions; +} + +/// Current information about a [SyncStream] that the sync client is subscribed +/// to. +final class SyncStreamStatus { + /// If the [SyncStatus] is currently [SyncStatus.downloading], download + /// progress for this stream. + final ProgressWithOperations? progress; + final CoreActiveStreamSubscription _internal; + + /// The [SyncSubscriptionDescription] providing information about the current + /// stream state. + SyncSubscriptionDescription get subscription => _internal; + + /// The [StreamPriority] of the current stream. + /// + /// New data on higher-priority streams can interrupt lower-priority streams. + StreamPriority get priority => _internal.priority; + + SyncStreamStatus._(this._internal, SyncDownloadProgress? progress) + : progress = progress?._internal._forStream(_internal); +} + +@Deprecated('Use StreamPriority instead') +typedef BucketPriority = StreamPriority; + +/// The priority of a PowerSync stream. +extension type const StreamPriority._(int priorityNumber) { + static const _highest = 0; + + factory StreamPriority(int i) { + assert(i >= _highest); + return StreamPriority._(i); + } + + bool operator >(StreamPriority other) => comparator(this, other) > 0; + bool operator >=(StreamPriority other) => comparator(this, other) >= 0; + bool operator <(StreamPriority other) => comparator(this, other) < 0; + bool operator <=(StreamPriority other) => comparator(this, other) <= 0; + + /// A [Comparator] instance suitable for comparing [StreamPriority] values. + static int comparator(StreamPriority a, StreamPriority b) => + -a.priorityNumber.compareTo(b.priorityNumber); + + /// The priority used by PowerSync to indicate that a full sync was completed. + static const fullSyncPriority = StreamPriority._(2147483647); +} + +/// Partial information about the synchronization status for buckets within a +/// priority. +typedef SyncPriorityStatus = ({ + StreamPriority priority, + DateTime? lastSyncedAt, + bool? hasSynced, +}); + +/// Stats of the local upload queue. +class UploadQueueStats { + /// Number of records in the upload queue. + int count; + + /// Size of the upload queue in bytes. + int? size; + + UploadQueueStats({required this.count, this.size}); + + @override + String toString() { + if (size == null) { + return "UploadQueueStats"; + } else { + return "UploadQueueStats"; + } + } +} + +/// Per-bucket download progress information. +@internal +typedef BucketProgress = ({ + StreamPriority priority, + int atLast, + int sinceLast, + int targetCount, +}); + +@internal +final class InternalSyncDownloadProgress extends ProgressWithOperations { + final Map buckets; + + InternalSyncDownloadProgress(this.buckets) + : super._( + buckets.values.map((e) => e.targetCount - e.atLast).sum, + buckets.values.map((e) => e.sinceLast).sum, + ); + + factory InternalSyncDownloadProgress.forNewCheckpoint( + Map localProgress, Checkpoint target) { + final buckets = {}; + + for (final bucket in target.checksums) { + final savedProgress = localProgress[bucket.bucket]; + final atLast = savedProgress?.atLast ?? 0; + final sinceLast = savedProgress?.sinceLast ?? 0; + + buckets[bucket.bucket] = ( + priority: StreamPriority._(bucket.priority), + atLast: atLast, + sinceLast: sinceLast, + targetCount: bucket.count ?? 0, + ); + + if (bucket.count case final knownCount?) { + if (knownCount < atLast + sinceLast) { + // Either due to a defrag / sync rule deploy or a compaction + // operation, the size of the bucket shrank so much that the local ops + // exceed the ops in the updated bucket. We can't possibly report + // progress in this case (it would overshoot 100%). + return InternalSyncDownloadProgress({ + for (final bucket in target.checksums) + bucket.bucket: ( + priority: StreamPriority(bucket.priority), + atLast: 0, + sinceLast: 0, + targetCount: knownCount, + ) + }); + } + } + } + + return InternalSyncDownloadProgress(buckets); + } + + static InternalSyncDownloadProgress ofPublic(SyncDownloadProgress public) { + return public._internal; + } + + /// Sums the total target and completed operations for all buckets up until + /// the given [priority] (inclusive). + ProgressWithOperations untilPriority(StreamPriority priority) { + final (total, downloaded) = buckets.values + .where((e) => e.priority >= priority) + .fold((0, 0), _addProgress); + + return ProgressWithOperations._(total, downloaded); + } + + ProgressWithOperations _forStream(CoreActiveStreamSubscription subscription) { + final (:total, :downloaded) = subscription.progress; + return ProgressWithOperations._(total, downloaded); + } + + InternalSyncDownloadProgress incrementDownloaded(SyncDataBatch batch) { + final newBucketStates = Map.of(buckets); + for (final dataForBucket in batch.buckets) { + final previous = newBucketStates[dataForBucket.bucket]!; + newBucketStates[dataForBucket.bucket] = ( + priority: previous.priority, + atLast: previous.atLast, + sinceLast: min(previous.sinceLast + dataForBucket.data.length, + previous.targetCount - previous.atLast), + targetCount: previous.targetCount, + ); + } + + return InternalSyncDownloadProgress(newBucketStates); + } + + SyncDownloadProgress get asSyncDownloadProgress => + SyncDownloadProgress._(this); + + @override + int get hashCode => _mapEquality.hash(buckets); + + @override + bool operator ==(Object other) { + return other is InternalSyncDownloadProgress && + // totalOperations and downloadedOperations are derived values, but + // comparing them first helps find a difference faster. + totalOperations == other.totalOperations && + downloadedOperations == other.downloadedOperations && + _mapEquality.equals(buckets, other.buckets); + } + + @override + String toString() { + final all = asSyncDownloadProgress; + return 'for total: ${all.downloadedOperations} / ${all.totalOperations}'; + } + + static const _mapEquality = MapEquality(); + + (int, int) _addProgress((int, int) prev, BucketProgress entry) { + final downloaded = entry.sinceLast; + final total = entry.targetCount - entry.atLast; + return (prev.$1 + total, prev.$2 + downloaded); + } +} + +/// Information about a progressing download. +/// +/// This reports the `total` amount of operations to download, how many of them +/// have already been `completed` and finally a `fraction` indicating relative +/// progress (as a number between `0.0` and `1.0`, inclusive) +/// +/// To obtain these values, use [SyncDownloadProgress] available through +/// [SyncStatus.downloadProgress]. +final class ProgressWithOperations { + /// How many operations need to be downloaded in total until the current + /// download is complete. + final int totalOperations; + + /// How many operations have already been downloaded since the last complete + /// download. + final int downloadedOperations; + + ProgressWithOperations._(this.totalOperations, this.downloadedOperations); + + /// Relative progress (as a number between `0.0` and `1.0`). + /// + /// When this number reaches `1.0`, all changes have been received from the + /// sync service. Actually applying these changes happens before the + /// [SyncStatus.downloadProgress] flag is cleared though, so progress can stay + /// at `1.0` for a short while before completing. + double get downloadedFraction { + return totalOperations == 0 ? 0.0 : downloadedOperations / totalOperations; + } +} + +/// Provides realtime progress on how PowerSync is downloading rows. +/// +/// This type reports progress by implementing [ProgressWithOperations], meaning +/// that [downloadedOperations], [totalOperations] and [downloadedFraction] are +/// available on instances of [SyncDownloadProgress]. +/// Additionally, it's possible to obtain the progress towards a specific +/// priority only (instead of tracking progress for the entire download) by +/// using [untilPriority]. +/// +/// The reported progress always reflects the status towards the end of a +/// sync iteration (after which a consistent snapshot of all buckets is +/// available locally). +/// +/// In rare cases (in particular, when a [compacting] operation takes place +/// between syncs), it's possible for the returned numbers to be slightly +/// inaccurate. For this reason, [SyncDownloadProgress] should be seen as an +/// approximation of progress. The information returned is good enough to build +/// progress bars, but not exact enough to track individual download counts. +/// +/// Also note that data is downloaded in bulk, which means that individual +/// counters are unlikely to be updated one-by-one. +/// +/// [compacting]: https://docs.powersync.com/usage/lifecycle-maintenance/compacting-buckets +extension type SyncDownloadProgress._(InternalSyncDownloadProgress _internal) + implements ProgressWithOperations { + /// Returns download progress towards all data up until the specified + /// [priority] being received. + /// + /// The returned [ProgressWithOperations] tracks the target amount of + /// operations that need to be downloaded in total and how many of them have + /// already been received. + ProgressWithOperations untilPriority(StreamPriority priority) { + return _internal.untilPriority(priority); + } +} diff --git a/packages/powersync/lib/src/user_agent/user_agent.dart b/packages/powersync_core/lib/src/user_agent/user_agent.dart similarity index 72% rename from packages/powersync/lib/src/user_agent/user_agent.dart rename to packages/powersync_core/lib/src/user_agent/user_agent.dart index 8f5405e1..ffcae2bd 100644 --- a/packages/powersync/lib/src/user_agent/user_agent.dart +++ b/packages/powersync_core/lib/src/user_agent/user_agent.dart @@ -2,4 +2,4 @@ export './user_agent_stub.dart' // ignore: uri_does_not_exist if (dart.library.io) './user_agent_native.dart' // ignore: uri_does_not_exist - if (dart.library.html) './user_agent_web.dart'; + if (dart.library.js_interop) './user_agent_web.dart'; diff --git a/packages/powersync/lib/src/user_agent/user_agent_native.dart b/packages/powersync_core/lib/src/user_agent/user_agent_native.dart similarity index 76% rename from packages/powersync/lib/src/user_agent/user_agent_native.dart rename to packages/powersync_core/lib/src/user_agent/user_agent_native.dart index 018ddf87..ab1638c5 100644 --- a/packages/powersync/lib/src/user_agent/user_agent_native.dart +++ b/packages/powersync_core/lib/src/user_agent/user_agent_native.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:powersync/src/version.dart'; +import 'package:powersync_core/src/version.dart'; String powerSyncUserAgent() { var dartVersion = RegExp(r'[\w.]+').stringMatch(Platform.version); @@ -10,7 +10,7 @@ String powerSyncUserAgent() { } // Ideally we'd get an OS version as well, but that's a little complex. // Platform.operatingSystemVersion is very verbose. - return 'powersync-dart/$libraryVersion $dart ${Platform.operatingSystem}'; + return 'powersync-dart-core/$libraryVersion $dart ${Platform.operatingSystem}'; } Map userAgentHeaders() { diff --git a/packages/powersync/lib/src/user_agent/user_agent_stub.dart b/packages/powersync_core/lib/src/user_agent/user_agent_stub.dart similarity index 100% rename from packages/powersync/lib/src/user_agent/user_agent_stub.dart rename to packages/powersync_core/lib/src/user_agent/user_agent_stub.dart diff --git a/packages/powersync/lib/src/user_agent/user_agent_web.dart b/packages/powersync_core/lib/src/user_agent/user_agent_web.dart similarity index 68% rename from packages/powersync/lib/src/user_agent/user_agent_web.dart rename to packages/powersync_core/lib/src/user_agent/user_agent_web.dart index fb9a9d6c..8475d63c 100644 --- a/packages/powersync/lib/src/user_agent/user_agent_web.dart +++ b/packages/powersync_core/lib/src/user_agent/user_agent_web.dart @@ -1,7 +1,7 @@ -import 'package:powersync/src/version.dart'; +import 'package:powersync_core/src/version.dart'; String powerSyncUserAgent() { - return 'powersync-dart/$libraryVersion Dart (flutter-web)'; + return 'powersync-dart-core/$libraryVersion Dart (flutter-web)'; } Map userAgentHeaders() { diff --git a/packages/powersync/lib/src/uuid.dart b/packages/powersync_core/lib/src/uuid.dart similarity index 100% rename from packages/powersync/lib/src/uuid.dart rename to packages/powersync_core/lib/src/uuid.dart diff --git a/packages/powersync_core/lib/src/version.dart b/packages/powersync_core/lib/src/version.dart new file mode 100644 index 00000000..211139eb --- /dev/null +++ b/packages/powersync_core/lib/src/version.dart @@ -0,0 +1 @@ +const String libraryVersion = '1.6.1'; diff --git a/packages/powersync_core/lib/src/web/powersync_db.worker.dart b/packages/powersync_core/lib/src/web/powersync_db.worker.dart new file mode 100644 index 00000000..4ae79365 --- /dev/null +++ b/packages/powersync_core/lib/src/web/powersync_db.worker.dart @@ -0,0 +1,13 @@ +/// This file needs to be compiled to JavaScript with the command +/// dart compile js -O4 packages/powersync/lib/src/web/powersync_db.worker.dart -o assets/db_worker.js +/// The output should then be included in each project's `web` directory + +library; + +import 'package:sqlite_async/sqlite3_web.dart'; + +import 'worker_utils.dart'; + +void main() { + WebSqlite.workerEntrypoint(controller: PowerSyncAsyncSqliteController()); +} diff --git a/packages/powersync/lib/src/web/sync_controller.dart b/packages/powersync_core/lib/src/web/sync_controller.dart similarity index 68% rename from packages/powersync/lib/src/web/sync_controller.dart rename to packages/powersync_core/lib/src/web/sync_controller.dart index 8231e7ce..b3f0ef18 100644 --- a/packages/powersync/lib/src/web/sync_controller.dart +++ b/packages/powersync_core/lib/src/web/sync_controller.dart @@ -1,34 +1,36 @@ import 'dart:async'; import 'dart:js_interop'; -import 'package:powersync/powersync.dart'; +import 'package:powersync_core/powersync_core.dart'; +import 'package:powersync_core/src/sync/options.dart'; import 'package:sqlite_async/web.dart'; import 'package:web/web.dart'; import '../database/web/web_powersync_database.dart'; -import '../streaming_sync.dart'; +import '../sync/streaming_sync.dart'; import 'sync_worker_protocol.dart'; class SyncWorkerHandle implements StreamingSync { final PowerSyncDatabaseImpl database; final PowerSyncBackendConnector connector; - final int crudThrottleTimeMs; - final Map? syncParams; - + final SyncOptions options; late final WorkerCommunicationChannel _channel; + List subscriptions; final StreamController _status = StreamController.broadcast(); - SyncWorkerHandle._( - {required this.database, - required this.connector, - required this.crudThrottleTimeMs, - required MessagePort sendToWorker, - required SharedWorker worker, - this.syncParams}) { + SyncWorkerHandle._({ + required this.database, + required this.connector, + required this.options, + required MessagePort sendToWorker, + required SharedWorker worker, + required this.subscriptions, + }) { _channel = WorkerCommunicationChannel( port: sendToWorker, errors: EventStreamProviders.errorEvent.forTarget(worker), + logger: database.logger, requestHandler: (type, payload) async { switch (type) { case SyncWorkerMessageType.requestEndpoint: @@ -47,7 +49,7 @@ class SyncWorkerHandle implements StreamingSync { await connector.uploadData(database); return (JSObject(), null); case SyncWorkerMessageType.invalidCredentialsCallback: - final credentials = await connector.fetchCredentials(); + final credentials = await connector.prefetchCredentials(); return ( credentials != null ? SerializedCredentials.from(credentials) @@ -76,20 +78,22 @@ class SyncWorkerHandle implements StreamingSync { }); } - static Future start( - {required PowerSyncDatabaseImpl database, - required PowerSyncBackendConnector connector, - required int crudThrottleTimeMs, - required Uri workerUri, - Map? syncParams}) async { + static Future start({ + required PowerSyncDatabaseImpl database, + required PowerSyncBackendConnector connector, + required Uri workerUri, + required SyncOptions options, + required List subscriptions, + }) async { final worker = SharedWorker(workerUri.toString().toJS); final handle = SyncWorkerHandle._( - database: database, - connector: connector, - crudThrottleTimeMs: crudThrottleTimeMs, - sendToWorker: worker.port, - worker: worker, - syncParams: syncParams); + options: options, + database: database, + connector: connector, + sendToWorker: worker.port, + worker: worker, + subscriptions: subscriptions, + ); // Make sure that the worker is working, or throw immediately. await handle._channel.ping(); @@ -113,6 +117,16 @@ class SyncWorkerHandle implements StreamingSync { @override Future streamingSync() async { await _channel.startSynchronization( - database.openFactory.path, crudThrottleTimeMs, syncParams); + database.database.openFactory.path, + ResolvedSyncOptions(options), + database.schema, + subscriptions, + ); + } + + @override + void updateSubscriptions(List streams) { + subscriptions = streams; + _channel.updateSubscriptions(streams); } } diff --git a/packages/powersync/lib/src/web/sync_worker.dart b/packages/powersync_core/lib/src/web/sync_worker.dart similarity index 62% rename from packages/powersync/lib/src/web/sync_worker.dart rename to packages/powersync_core/lib/src/web/sync_worker.dart index 0b405bb1..1c92808f 100644 --- a/packages/powersync/lib/src/web/sync_worker.dart +++ b/packages/powersync_core/lib/src/web/sync_worker.dart @@ -8,11 +8,15 @@ import 'dart:convert'; import 'dart:js_interop'; import 'package:async/async.dart'; -import 'package:fetch_client/fetch_client.dart'; -import 'package:powersync/powersync.dart'; -import 'package:powersync/sqlite_async.dart'; -import 'package:powersync/src/database/powersync_db_mixin.dart'; -import 'package:powersync/src/streaming_sync.dart'; +import 'package:collection/collection.dart'; +import 'package:http/browser_client.dart'; +import 'package:logging/logging.dart'; +import 'package:powersync_core/powersync_core.dart'; +import 'package:powersync_core/sqlite_async.dart'; +import 'package:powersync_core/src/database/powersync_db_mixin.dart'; +import 'package:powersync_core/src/sync/internal_connector.dart'; +import 'package:powersync_core/src/sync/options.dart'; +import 'package:powersync_core/src/sync/streaming_sync.dart'; import 'package:sqlite_async/web.dart'; import 'package:web/web.dart' hide RequestMode; @@ -44,13 +48,19 @@ class _SyncWorker { _SyncRunner referenceSyncTask( String databaseIdentifier, - int crudThrottleTimeMs, - String? syncParamsEncoded, + SyncOptions options, + String schemaJson, + List subscriptions, _ConnectedClient client) { return _requestedSyncTasks.putIfAbsent(databaseIdentifier, () { return _SyncRunner(databaseIdentifier); }) - ..registerClient(client, crudThrottleTimeMs, syncParamsEncoded); + ..registerClient( + client, + options, + schemaJson, + subscriptions, + ); } } @@ -59,7 +69,7 @@ class _ConnectedClient { final _SyncWorker _worker; _SyncRunner? _runner; - StreamSubscription? _logSubscription; + StreamSubscription? _logSubscription; _ConnectedClient(MessagePort port, this._worker) { channel = WorkerCommunicationChannel( @@ -68,13 +78,40 @@ class _ConnectedClient { switch (type) { case SyncWorkerMessageType.startSynchronization: final request = payload as StartSynchronization; - _runner = _worker.referenceSyncTask(request.databaseName, - request.crudThrottleTimeMs, request.syncParamsEncoded, this); + final recoveredOptions = SyncOptions( + crudThrottleTime: + Duration(milliseconds: request.crudThrottleTimeMs), + retryDelay: switch (request.retryDelayMs) { + null => null, + final retryDelay => Duration(milliseconds: retryDelay), + }, + params: switch (request.syncParamsEncoded) { + null => null, + final encodedParams => + jsonDecode(encodedParams) as Map, + }, + syncImplementation: switch (request.implementationName) { + null => SyncClientImplementation.defaultClient, + final name => SyncClientImplementation.values.byName(name), + }, + ); + + _runner = _worker.referenceSyncTask( + request.databaseName, + recoveredOptions, + request.schemaJson, + request.subscriptions?.toDart ?? const [], + this, + ); return (JSObject(), null); case SyncWorkerMessageType.abortSynchronization: _runner?.disconnectClient(this); _runner = null; return (JSObject(), null); + case SyncWorkerMessageType.updateSubscriptions: + _runner?.updateClientSubscriptions( + this, (payload as UpdateSubscriptions).toDart); + return (JSObject(), null); default: throw StateError('Unexpected message type $type'); } @@ -109,15 +146,16 @@ class _ConnectedClient { class _SyncRunner { final String identifier; - int crudThrottleTimeMs = 1; - String? syncParamsEncoded; + ResolvedSyncOptions options = ResolvedSyncOptions(SyncOptions()); + String schemaJson = '{}'; final StreamGroup<_RunnerEvent> _group = StreamGroup(); final StreamController<_RunnerEvent> _mainEvents = StreamController(); - StreamingSync? sync; + StreamingSyncImplementation? sync; _ConnectedClient? databaseHost; - final connections = <_ConnectedClient>[]; + final connections = <_ConnectedClient, List>{}; + List currentStreams = []; _SyncRunner(this.identifier) { _group.add(_mainEvents.stream); @@ -128,19 +166,15 @@ class _SyncRunner { switch (event) { case _AddConnection( :final client, - :final crudThrottleTimeMs, - :final syncParamsEncoded + :final options, + :final schemaJson, + :final subscriptions, ): - connections.add(client); - var reconnect = false; - if (this.crudThrottleTimeMs != crudThrottleTimeMs) { - this.crudThrottleTimeMs = crudThrottleTimeMs; - reconnect = true; - } - if (this.syncParamsEncoded != syncParamsEncoded) { - this.syncParamsEncoded = syncParamsEncoded; - reconnect = true; - } + connections[client] = subscriptions; + final (newOptions, reconnect) = this.options.applyFrom(options); + this.options = newOptions; + this.schemaJson = schemaJson; + if (sync == null) { await _requestDatabase(client); } else if (reconnect) { @@ -148,6 +182,8 @@ class _SyncRunner { sync?.abort(); sync = null; await _requestDatabase(client); + } else { + reindexSubscriptions(); } case _RemoveConnection(:final client): connections.remove(client); @@ -174,6 +210,12 @@ class _SyncRunner { } else { await _requestDatabase(newHost); } + case _ClientSubscriptionsChanged( + :final client, + :final subscriptions + ): + connections[client] = subscriptions; + reindexSubscriptions(); } } catch (e, s) { _logger.warning('Error handling $event', e, s); @@ -182,12 +224,24 @@ class _SyncRunner { }); } + /// Updates [currentStreams] to the union of values in [connections]. + void reindexSubscriptions() { + final before = currentStreams.toSet(); + final after = connections.values.flattenedToSet; + if (!const SetEquality().equals(before, after)) { + _logger.info( + 'Subscriptions across tabs have changed, checking whether a reconnect is necessary'); + currentStreams = after.toList(); + sync?.updateSubscriptions(currentStreams); + } + } + /// Pings all current [connections], removing those that don't answer in 5s /// (as they are likely closed tabs as well). /// /// Returns the first client that responds (without waiting for others). Future<_ConnectedClient?> _collectActiveClients() async { - final candidates = connections.toList(); + final candidates = connections.keys.toList(); if (candidates.isEmpty) { return null; } @@ -240,7 +294,6 @@ class _SyncRunner { }); final tables = ['ps_crud']; - final crudThrottleTime = Duration(milliseconds: crudThrottleTimeMs); Stream crudStream = powerSyncUpdateNotifications(Stream.empty()); if (database.updates != null) { @@ -248,28 +301,32 @@ class _SyncRunner { .transform(UpdateNotification.filterTablesTransformer(tables)); crudStream = UpdateNotification.throttleStream( filteredStream, - crudThrottleTime, + options.crudThrottleTime, addOne: UpdateNotification.empty(), ); } - final syncParams = syncParamsEncoded == null - ? null - : jsonDecode(syncParamsEncoded!) as Map; - + currentStreams = connections.values.flattenedToSet.toList(); sync = StreamingSyncImplementation( - adapter: WebBucketStorage(database), - credentialsCallback: client.channel.credentialsCallback, - invalidCredentialsCallback: client.channel.invalidCredentialsCallback, + adapter: WebBucketStorage(database), + schemaJson: client._runner!.schemaJson, + connector: InternalConnector( + getCredentialsCached: client.channel.credentialsCallback, + prefetchCredentials: ({required bool invalidate}) async { + return await client.channel.invalidCredentialsCallback(); + }, uploadCrud: client.channel.uploadCrud, - crudUpdateTriggerStream: crudStream, - retryDelay: Duration(seconds: 3), - client: FetchClient(mode: RequestMode.cors), - identifier: identifier, - syncParameters: syncParams); + ), + crudUpdateTriggerStream: crudStream, + options: options, + client: BrowserClient(), + identifier: identifier, + activeSubscriptions: currentStreams, + logger: _logger, + ); sync!.statusStream.listen((event) { _logger.fine('Broadcasting sync event: $event'); - for (final client in connections) { + for (final client in connections.keys) { client.channel.notify(SyncWorkerMessageType.notifySyncStatus, SerializedSyncStatus.from(event)); } @@ -277,10 +334,9 @@ class _SyncRunner { sync!.streamingSync(); } - void registerClient(_ConnectedClient client, int currentCrudThrottleTimeMs, - String? currentSyncParamsEncoded) { - _mainEvents.add(_AddConnection( - client, currentCrudThrottleTimeMs, currentSyncParamsEncoded)); + void registerClient(_ConnectedClient client, SyncOptions options, + String schemaJson, List subscriptions) { + _mainEvents.add(_AddConnection(client, options, schemaJson, subscriptions)); } /// Remove a client, disconnecting if no clients remain.. @@ -292,16 +348,23 @@ class _SyncRunner { void disconnectClient(_ConnectedClient client) { _mainEvents.add(_DisconnectClient(client)); } + + void updateClientSubscriptions( + _ConnectedClient client, List subscriptions) { + _mainEvents.add(_ClientSubscriptionsChanged(client, subscriptions)); + } } sealed class _RunnerEvent {} final class _AddConnection implements _RunnerEvent { final _ConnectedClient client; - final int crudThrottleTimeMs; - final String? syncParamsEncoded; + final SyncOptions options; + final String schemaJson; + final List subscriptions; - _AddConnection(this.client, this.crudThrottleTimeMs, this.syncParamsEncoded); + _AddConnection( + this.client, this.options, this.schemaJson, this.subscriptions); } final class _RemoveConnection implements _RunnerEvent { @@ -316,6 +379,13 @@ final class _DisconnectClient implements _RunnerEvent { _DisconnectClient(this.client); } +final class _ClientSubscriptionsChanged implements _RunnerEvent { + final _ConnectedClient client; + final List subscriptions; + + _ClientSubscriptionsChanged(this.client, this.subscriptions); +} + final class _ActiveDatabaseClosed implements _RunnerEvent { const _ActiveDatabaseClosed(); } diff --git a/packages/powersync/lib/src/web/sync_worker_protocol.dart b/packages/powersync_core/lib/src/web/sync_worker_protocol.dart similarity index 63% rename from packages/powersync/lib/src/web/sync_worker_protocol.dart rename to packages/powersync_core/lib/src/web/sync_worker_protocol.dart index f23a18bc..0448fe5a 100644 --- a/packages/powersync/lib/src/web/sync_worker_protocol.dart +++ b/packages/powersync_core/lib/src/web/sync_worker_protocol.dart @@ -2,11 +2,16 @@ import 'dart:async'; import 'dart:convert'; import 'dart:js_interop'; +import 'package:logging/logging.dart'; +import 'package:powersync_core/src/schema.dart'; +import 'package:powersync_core/src/sync/options.dart'; +import 'package:powersync_core/src/sync/stream.dart'; import 'package:web/web.dart'; import '../connector.dart'; import '../log.dart'; -import '../sync_status.dart'; +import '../sync/streaming_sync.dart'; +import '../sync/sync_status.dart'; /// Names used in [SyncWorkerMessage] enum SyncWorkerMessageType { @@ -17,6 +22,9 @@ enum SyncWorkerMessageType { /// If parameters change, the sync worker reconnects. startSynchronization, + /// Update the active subscriptions that this client is interested in. + updateSubscriptions, + /// The [SyncWorkerMessage.payload] for the request is a numeric id, the /// response can be anything (void). /// This disconnects immediately, even if other clients are still open. @@ -63,16 +71,54 @@ extension type SyncWorkerMessage._(JSObject _) implements JSObject { @anonymous extension type StartSynchronization._(JSObject _) implements JSObject { - external factory StartSynchronization( - {required String databaseName, - required int crudThrottleTimeMs, - required int requestId, - String? syncParamsEncoded}); + external factory StartSynchronization({ + required String databaseName, + required int crudThrottleTimeMs, + required int requestId, + required int retryDelayMs, + required String implementationName, + required String schemaJson, + String? syncParamsEncoded, + UpdateSubscriptions? subscriptions, + }); external String get databaseName; external int get requestId; external int get crudThrottleTimeMs; + external int? get retryDelayMs; + external String? get implementationName; + external String get schemaJson; external String? get syncParamsEncoded; + external UpdateSubscriptions? get subscriptions; +} + +@anonymous +extension type UpdateSubscriptions._raw(JSObject _inner) implements JSObject { + external factory UpdateSubscriptions._({ + required int requestId, + required JSArray content, + }); + + factory UpdateSubscriptions(int requestId, List streams) { + return UpdateSubscriptions._( + requestId: requestId, + content: streams + .map((e) => [e.name.toJS, e.parameters.toJS].toJS) + .toList() + .toJS, + ); + } + + external int get requestId; + external JSArray get content; + + List get toDart { + return content.toDart.map((e) { + final [name, parameters] = (e as JSArray).toDart; + + return (name: name.toDart, parameters: parameters.toDart); + }).toList(); + } } @anonymous @@ -145,6 +191,50 @@ extension type SerializedCredentials._(JSObject _) implements JSObject { } } +@anonymous +extension type SerializedBucketProgress._(JSObject _) implements JSObject { + external factory SerializedBucketProgress({ + required String name, + required int priority, + required int atLast, + required int sinceLast, + required int targetCount, + }); + + external String name; + external int priority; + external int atLast; + external int sinceLast; + external int targetCount; + + static JSArray serialize( + Map buckets) { + return [ + for (final MapEntry(:key, :value) in buckets.entries) + SerializedBucketProgress( + name: key, + priority: value.priority.priorityNumber, + atLast: value.atLast, + sinceLast: value.sinceLast, + targetCount: value.targetCount, + ), + ].toJS; + } + + static Map deserialize( + JSArray array) { + return { + for (final entry in array.toDart) + entry.name: ( + priority: StreamPriority(entry.priority), + atLast: entry.atLast, + sinceLast: entry.sinceLast, + targetCount: entry.targetCount, + ), + }; + } +} + @anonymous extension type SerializedSyncStatus._(JSObject _) implements JSObject { external factory SerializedSyncStatus({ @@ -156,6 +246,9 @@ extension type SerializedSyncStatus._(JSObject _) implements JSObject { required bool? hasSyned, required String? uploadError, required String? downloadError, + required JSArray? priorityStatusEntries, + required JSArray? syncProgress, + required JSString streamSubscriptions, }); factory SerializedSyncStatus.from(SyncStatus status) { @@ -168,6 +261,20 @@ extension type SerializedSyncStatus._(JSObject _) implements JSObject { hasSyned: status.hasSynced, uploadError: status.uploadError?.toString(), downloadError: status.downloadError?.toString(), + priorityStatusEntries: [ + for (final entry in status.priorityStatusEntries) + [ + entry.priority.priorityNumber.toJS, + entry.lastSyncedAt?.microsecondsSinceEpoch.toJS, + entry.hasSynced?.toJS, + ].toJS + ].toJS, + syncProgress: switch (status.downloadProgress) { + null => null, + var other => SerializedBucketProgress.serialize( + InternalSyncDownloadProgress.ofPublic(other).buckets), + }, + streamSubscriptions: json.encode(status.internalSubscriptions).toJS, ); } @@ -179,8 +286,13 @@ extension type SerializedSyncStatus._(JSObject _) implements JSObject { external bool? hasSynced; external String? uploadError; external String? downloadError; + external JSArray? priorityStatusEntries; + external JSArray? syncProgress; + external JSString? streamSubscriptions; SyncStatus asSyncStatus() { + final streamSubscriptions = this.streamSubscriptions?.toDart; + return SyncStatus( connected: connected, connecting: connecting, @@ -192,24 +304,52 @@ extension type SerializedSyncStatus._(JSObject _) implements JSObject { hasSynced: hasSynced, uploadError: uploadError, downloadError: downloadError, + priorityStatusEntries: [ + if (priorityStatusEntries case final jsEntries?) + ...jsEntries.toDart.map((e) { + final [rawPriority, rawSynced, rawHasSynced, ...] = + (e as JSArray).toDart; + final syncedMillis = (rawSynced as JSNumber?)?.toDartInt; + + return ( + priority: StreamPriority((rawPriority as JSNumber).toDartInt), + lastSyncedAt: syncedMillis != null + ? DateTime.fromMicrosecondsSinceEpoch(syncedMillis) + : null, + hasSynced: (rawHasSynced as JSBoolean?)?.toDart, + ); + }) + ], + downloadProgress: switch (syncProgress) { + null => null, + final serializedProgress => InternalSyncDownloadProgress( + SerializedBucketProgress.deserialize(serializedProgress)) + .asSyncDownloadProgress, + }, + streamSubscriptions: switch (streamSubscriptions) { + null => null, + final serialized => (json.decode(serialized) as List?) + ?.map((e) => CoreActiveStreamSubscription.fromJson( + e as Map)) + .toList(), + }, ); } } final class WorkerCommunicationChannel { - static final _logger = autoLogger; - final Map> _pendingRequests = {}; int _nextRequestId = 0; bool _hasError = false; - StreamSubscription? _incomingMessages; - StreamSubscription? _incomingErrors; + StreamSubscription? _incomingMessages; + StreamSubscription? _incomingErrors; final MessagePort port; final FutureOr<(JSAny?, JSArray?)> Function(SyncWorkerMessageType, JSAny) requestHandler; final StreamController<(SyncWorkerMessageType, JSAny)> _events = StreamController(); + final Logger _logger; Stream<(SyncWorkerMessageType, JSAny)> get events => _events.stream; @@ -217,7 +357,8 @@ final class WorkerCommunicationChannel { required this.port, required this.requestHandler, Stream? errors, - }) { + Logger? logger, + }) : _logger = logger ?? autoLogger { port.start(); _incomingErrors = errors?.listen((event) { _hasError = true; @@ -246,6 +387,8 @@ final class WorkerCommunicationChannel { return; case SyncWorkerMessageType.startSynchronization: requestId = (message.payload as StartSynchronization).requestId; + case SyncWorkerMessageType.updateSubscriptions: + requestId = (message.payload as UpdateSubscriptions).requestId; case SyncWorkerMessageType.requestEndpoint: case SyncWorkerMessageType.abortSynchronization: case SyncWorkerMessageType.credentialsCallback: @@ -319,18 +462,39 @@ final class WorkerCommunicationChannel { await _numericRequest(SyncWorkerMessageType.ping); } - Future startSynchronization(String databaseName, int crudThrottleTimeMs, - Map? syncParams) async { + Future startSynchronization( + String databaseName, + ResolvedSyncOptions options, + Schema schema, + List streams, + ) async { final (id, completion) = _newRequest(); port.postMessage(SyncWorkerMessage( type: SyncWorkerMessageType.startSynchronization.name, payload: StartSynchronization( - databaseName: databaseName, - crudThrottleTimeMs: crudThrottleTimeMs, - requestId: id, - syncParamsEncoded: - syncParams == null ? null : jsonEncode(syncParams)), + databaseName: databaseName, + crudThrottleTimeMs: options.crudThrottleTime.inMilliseconds, + retryDelayMs: options.retryDelay.inMilliseconds, + requestId: id, + implementationName: options.source.syncImplementation.name, + schemaJson: jsonEncode(schema), + syncParamsEncoded: switch (options.source.params) { + null => null, + final params => jsonEncode(params), + }, + subscriptions: UpdateSubscriptions(-1, streams), + ), + )); + await completion; + } + + Future updateSubscriptions(List streams) async { + final (id, completion) = _newRequest(); + port.postMessage(SyncWorkerMessage( + type: SyncWorkerMessageType.updateSubscriptions.name, + payload: UpdateSubscriptions(id, streams), )); + await completion; } diff --git a/packages/powersync/lib/src/web/web_bucket_storage.dart b/packages/powersync_core/lib/src/web/web_bucket_storage.dart similarity index 72% rename from packages/powersync/lib/src/web/web_bucket_storage.dart rename to packages/powersync_core/lib/src/web/web_bucket_storage.dart index ba106b26..a430d569 100644 --- a/packages/powersync/lib/src/web/web_bucket_storage.dart +++ b/packages/powersync_core/lib/src/web/web_bucket_storage.dart @@ -1,5 +1,5 @@ -import 'package:powersync/sqlite_async.dart'; -import 'package:powersync/src/bucket_storage.dart'; +import 'package:powersync_core/sqlite_async.dart'; +import 'package:powersync_core/src/sync/bucket_storage.dart'; import 'package:sqlite_async/web.dart'; class WebBucketStorage extends BucketStorage { @@ -17,4 +17,9 @@ class WebBucketStorage extends BucketStorage { return _webDb.writeTransaction(callback, lockTimeout: lockTimeout, flush: flush); } + + @override + Future flushFileSystem() { + return _webDb.flush(); + } } diff --git a/packages/powersync_core/lib/src/web/worker_utils.dart b/packages/powersync_core/lib/src/web/worker_utils.dart new file mode 100644 index 00000000..6f010219 --- /dev/null +++ b/packages/powersync_core/lib/src/web/worker_utils.dart @@ -0,0 +1,85 @@ +import 'dart:js_interop'; + +import 'package:powersync_core/src/open_factory/common_db_functions.dart'; +import 'package:sqlite_async/sqlite3_wasm.dart'; +import 'package:sqlite_async/sqlite3_web.dart'; +import 'package:sqlite_async/sqlite3_web_worker.dart'; +import 'package:uuid/uuid.dart'; + +final class PowerSyncAsyncSqliteController extends AsyncSqliteController { + @override + Future openDatabase(WasmSqlite3 sqlite3, String path, + String vfs, JSAny? additionalData) async { + final asyncDb = + await super.openDatabase(sqlite3, path, vfs, additionalData); + setupPowerSyncDatabase(asyncDb.database); + return asyncDb; + } + + @override + CommonDatabase openUnderlying( + WasmSqlite3 sqlite3, String path, String vfs, JSAny? additionalData) { + final options = additionalData == null + ? null + : additionalData as PowerSyncAdditionalOpenOptions; + if (options != null && options.useMultipleCiphersVfs) { + vfs = 'multipleciphers-$vfs'; + } + + return sqlite3.open(path, vfs: vfs); + } + + @override + Future handleCustomRequest( + ClientConnection connection, JSAny? request) { + throw UnimplementedError(); + } +} + +@JS() +@anonymous +extension type PowerSyncAdditionalOpenOptions._(JSObject _) + implements JSObject { + external factory PowerSyncAdditionalOpenOptions({ + required bool useMultipleCiphersVfs, + }); + + external bool get useMultipleCiphersVfs; +} + +// Registers custom SQLite functions for the SQLite connection +void setupPowerSyncDatabase(CommonDatabase database) { + setupCommonDBFunctions(database); + final uuid = Uuid(); + + database.createFunction( + functionName: 'uuid', + argumentCount: const AllowedArgumentCount(0), + function: (args) { + return uuid.v4(); + }, + ); + database.createFunction( + // Postgres compatibility + functionName: 'gen_random_uuid', + argumentCount: const AllowedArgumentCount(0), + function: (args) => uuid.v4(), + ); + database.createFunction( + functionName: 'powersync_sleep', + argumentCount: const AllowedArgumentCount(1), + function: (args) { + // Can't perform synchronous sleep on web + final millis = args[0] as int; + return millis; + }, + ); + + database.createFunction( + functionName: 'powersync_connection_name', + argumentCount: const AllowedArgumentCount(0), + function: (args) { + return 'N/A'; + }, + ); +} diff --git a/packages/powersync_core/lib/web.dart b/packages/powersync_core/lib/web.dart new file mode 100644 index 00000000..fbf8598e --- /dev/null +++ b/packages/powersync_core/lib/web.dart @@ -0,0 +1,19 @@ +/// Internal options used to customize how PowerSync opens databases on the web. +library; + +export 'src/web/worker_utils.dart' show PowerSyncAdditionalOpenOptions; +export 'package:sqlite_async/sqlite3_web.dart'; +export 'package:sqlite_async/web.dart'; + +import 'package:sqlite_async/web.dart'; +import 'powersync_core.dart' as core; +import 'src/open_factory/web/web_open_factory.dart'; + +/// The default [core.PowerSyncOpenFactory] implementation for the web. Unlike +/// the cross-platform interface, this is guaranteed to implement +/// [WebSqliteOpenFactory]. +/// +/// This typedef is mostly used internally, e.g. in the web implementation of +/// `powersync_sqlcipher` which relies on the fact that web-specific factory +/// methods are available. +typedef PowerSyncWebOpenFactory = PowerSyncOpenFactory; diff --git a/packages/powersync_core/pubspec.yaml b/packages/powersync_core/pubspec.yaml new file mode 100644 index 00000000..723cd4e9 --- /dev/null +++ b/packages/powersync_core/pubspec.yaml @@ -0,0 +1,51 @@ +name: powersync_core +version: 1.6.1 +homepage: https://powersync.com +repository: https://github.com/powersync-ja/powersync.dart +description: PowerSync Dart SDK - sync engine for building local-first apps. + +environment: + sdk: ^3.4.3 + +dependencies: + sqlite_async: ^0.12.1 + # We only use sqlite3 as a transitive dependency, + # but right now we need a minimum of v2.4.6. + sqlite3: ^2.4.6 + # We implement a database controller, which is an interface of sqlite3_web. + sqlite3_web: ^0.3.2 + meta: ^1.0.0 + http: ^1.5.0 + uuid: ^4.2.0 + async: ^2.10.0 + logging: ^1.1.1 + collection: ^1.19.0 + web: ^1.0.0 + + # Only used internally to download WASM / worker files. + args: ^2.6.0 + pub_semver: ^2.0.0 + pubspec_parse: ^1.3.0 + path: ^1.8.0 + typed_data: ^1.4.0 + +dev_dependencies: + lints: ^5.1.1 + test: ^1.25.0 + test_api: ^0.7.0 + shelf: ^1.4.1 + shelf_router: ^1.1.4 + shelf_static: ^1.1.2 + stream_channel: ^2.1.2 + fake_async: ^1.3.3 + bson: ^5.0.7 + test_descriptor: ^2.0.2 + mockito: ^5.5.0 + +platforms: + android: + ios: + linux: + macos: + windows: + web: diff --git a/packages/powersync_core/test/attachments/attachment_test.dart b/packages/powersync_core/test/attachments/attachment_test.dart new file mode 100644 index 00000000..90cd1f37 --- /dev/null +++ b/packages/powersync_core/test/attachments/attachment_test.dart @@ -0,0 +1,372 @@ +import 'dart:typed_data'; + +import 'package:async/async.dart'; +import 'package:logging/logging.dart'; +import 'package:mockito/mockito.dart'; +import 'package:powersync_core/attachments/attachments.dart'; +import 'package:powersync_core/powersync_core.dart'; +import 'package:test/test.dart'; + +import '../utils/abstract_test_utils.dart'; +import '../utils/test_utils_impl.dart'; + +void main() { + late TestPowerSyncFactory factory; + late PowerSyncDatabase db; + late MockRemoteStorage remoteStorage; + late LocalStorage localStorage; + late AttachmentQueue queue; + late StreamQueue> attachments; + + Stream> watchAttachments() { + return db + .watch('SELECT photo_id FROM users WHERE photo_id IS NOT NULL') + .map( + (rs) => [ + for (final row in rs) + WatchedAttachmentItem( + id: row['photo_id'] as String, fileExtension: 'jpg') + ], + ); + } + + setUpAll(() async { + factory = await TestUtils().testFactory(); + }); + + setUp(() async { + remoteStorage = MockRemoteStorage(); + localStorage = LocalStorage.inMemory(); + + final (raw, database) = await factory.openInMemoryDatabase( + schema: _schema, + // Uncomment to see test logs + logger: Logger.detached('PowerSyncTest'), + ); + await database.initialize(); + db = database; + + queue = AttachmentQueue( + db: db, + remoteStorage: remoteStorage, + watchAttachments: watchAttachments, + localStorage: localStorage, + archivedCacheLimit: 0, + ); + + attachments = StreamQueue(db.attachments); + await expectLater(attachments, emits(isEmpty)); + }); + + tearDown(() async { + await attachments.cancel(); + await queue.stopSyncing(); + await queue.close(); + + await db.close(); + }); + + test('downloads attachments', () async { + await queue.startSync(); + + // Create a user with a photo_id specified. Since we didn't save an + // attachment before assigning a photo_id, this is equivalent to reuqiring + // an attachment download. + await db.execute( + 'INSERT INTO users (id, name, email, photo_id) VALUES (uuid(), ?, ?, uuid())', + ['steven', 'steven@journeyapps.com'], + ); + + var [attachment] = await attachments.next; + if (attachment.state == AttachmentState.queuedDownload) { + // Depending on timing with the queue scanning for items asynchronously, + // we may see a queued download or a synced event initially. + [attachment] = await attachments.next; + } + + expect(attachment.state, AttachmentState.synced); + final localUri = attachment.localUri!; + + // A download should he been attempted for this file. + verify(remoteStorage.downloadFile(argThat(isAttachment(attachment.id)))); + + // A file should now exist. + expect(await localStorage.fileExists(localUri), isTrue); + + // Now clear the user's photo_id, which should archive the attachment. + await db.execute('UPDATE users SET photo_id = NULL'); + + var nextAttachment = (await attachments.next).firstOrNull; + if (nextAttachment != null) { + expect(nextAttachment.state, AttachmentState.archived); + nextAttachment = (await attachments.next).firstOrNull; + } + + expect(nextAttachment, isNull); + + // File should have been deleted too + expect(await localStorage.fileExists(localUri), isFalse); + }); + + test('stores relative paths', () async { + // Regression test we had in the Kotlin/Swift implementation: + // https://github.com/powersync-ja/powersync-swift/pull/74 + await queue.startSync(); + await db.execute( + 'INSERT INTO users (id, name, email, photo_id) VALUES (uuid(), ?, ?, ?)', + ['steven', 'steven@journeyapps.com', 'picture_id'], + ); + + // Wait for attachment to sync. + await expectLater( + attachments, + emitsThrough([ + isA() + .having((e) => e.state, 'state', AttachmentState.synced) + ])); + + expect(await localStorage.fileExists('picture_id.jpg'), isTrue); + }); + + test('recovers from deleted local files', () async { + // Create an attachments record which has an invalid local_uri. + await db.execute( + 'INSERT OR REPLACE INTO attachments_queue ' + '(id, timestamp, filename, local_uri, media_type, size, state, has_synced, meta_data) ' + 'VALUES (uuid(), current_timestamp, ?, ?, ?, ?, ?, ?, ?)', + [ + 'attachment.jpg', + 'invalid/dir/attachment.jpg', + 'application/jpeg', + 1, + AttachmentState.synced.toInt(), + 1, + "" + ], + ); + await attachments.next; + + queue = AttachmentQueue( + db: db, + remoteStorage: remoteStorage, + watchAttachments: watchAttachments, + localStorage: localStorage, + archivedCacheLimit: 1, + ); + + // The attachment should be marked as archived, and the local URI should be + // removed. + await queue.startSync(); + + final [attachment] = await attachments.next; + expect(attachment.filename, 'attachment.jpg'); + expect(attachment.localUri, isNull); + expect(attachment.state, AttachmentState.archived); + }); + + test('uploads attachments', () async { + await queue.startSync(); + + final record = await queue.saveFile( + data: Stream.value(Uint8List(123)), + mediaType: 'image/jpg', + updateHook: (tx, attachment) async { + await tx.execute( + 'INSERT INTO users (id, name, email, photo_id) VALUES (uuid(), ?, ?, ?);', + ['steven', 'steven@journeyapps.com', attachment.id], + ); + }, + ); + expect(record.size, 123); + + var [attachment] = await attachments.next; + if (attachment.state == AttachmentState.queuedUpload) { + // Wait for it to be synced + [attachment] = await attachments.next; + } + + expect(attachment.state, AttachmentState.synced); + + // An upload should have been attempted for this file. + verify(remoteStorage.uploadFile(any, argThat(isAttachment(record.id)))); + expect(await localStorage.fileExists(record.localUri!), isTrue); + + // Now clear the user's photo_id, which should archive the attachment. + await db.execute('UPDATE users SET photo_id = NULL'); + + // Should delete attachment from database + await expectLater(attachments, emitsThrough(isEmpty)); + + // File should have been deleted too + expect(await localStorage.fileExists(record.localUri!), isFalse); + }); + + test('delete attachments', () async { + await queue.startSync(); + + final id = await queue.generateAttachmentId(); + await db.execute( + 'INSERT INTO users (id, name, email, photo_id) VALUES (uuid(), ?, ?, ?)', + ['steven', 'steven@journeyapps.com', id], + ); + + // Wait for the attachment to be synced. + await expectLater( + attachments, + emitsThrough([ + isA() + .having((e) => e.state, 'state', AttachmentState.synced) + ]), + ); + + await queue.deleteFile( + attachmentId: id, + updateHook: (tx, attachment) async { + await tx.execute( + 'UPDATE users SET photo_id = NULL WHERE photo_id = ?', + [attachment.id], + ); + }, + ); + + // Record should be deleted. + await expectLater(attachments, emitsThrough(isEmpty)); + verify(remoteStorage.deleteFile(argThat(isAttachment(id)))); + }); + + test('cached download', () async { + queue = AttachmentQueue( + db: db, + remoteStorage: remoteStorage, + watchAttachments: watchAttachments, + localStorage: localStorage, + archivedCacheLimit: 10, + ); + + await queue.startSync(); + + // Create attachment and wait for download. + await db.execute( + 'INSERT INTO users (id, name, email, photo_id) VALUES (uuid(), ?, ?, uuid())', + ['steven', 'steven@journeyapps.com'], + ); + await expectLater( + attachments, + emitsThrough([ + isA() + .having((e) => e.state, 'state', AttachmentState.synced) + ]), + ); + final [id as String, localUri as String] = + (await db.get('SELECT id, local_uri FROM attachments_queue')).values; + verify(remoteStorage.downloadFile(argThat(isAttachment(id)))); + expect(await localStorage.fileExists(localUri), isTrue); + + // Archive attachment by not referencing it anymore. + await db.execute('UPDATE users SET photo_id = NULL'); + await expectLater( + attachments, + emitsThrough([ + isA() + .having((e) => e.state, 'state', AttachmentState.archived) + ]), + ); + + // Restore from cache + await db.execute('UPDATE users SET photo_id = ?', [id]); + await expectLater( + attachments, + emitsThrough([ + isA() + .having((e) => e.state, 'state', AttachmentState.synced) + ]), + ); + expect(await localStorage.fileExists(localUri), isTrue); + + // Should not have downloaded attachment again because we have it locally. + verifyNoMoreInteractions(remoteStorage); + }); + + test('skip failed download', () async { + Future errorHandler( + Attachment attachment, Object exception, StackTrace trace) async { + return false; + } + + queue = AttachmentQueue( + db: db, + remoteStorage: remoteStorage, + watchAttachments: watchAttachments, + localStorage: localStorage, + errorHandler: AttachmentErrorHandler( + onDeleteError: expectAsync3(errorHandler, count: 0), + onDownloadError: expectAsync3(errorHandler, count: 1), + onUploadError: expectAsync3(errorHandler, count: 0), + ), + ); + + when(remoteStorage.downloadFile(any)).thenAnswer((_) async { + throw 'test error'; + }); + + await queue.startSync(); + await db.execute( + 'INSERT INTO users (id, name, email, photo_id) VALUES (uuid(), ?, ?, uuid())', + ['steven', 'steven@journeyapps.com'], + ); + + expect(await attachments.next, [ + isA() + .having((e) => e.state, 'state', AttachmentState.queuedDownload) + ]); + expect(await attachments.next, [ + isA() + .having((e) => e.state, 'state', AttachmentState.archived) + ]); + }); +} + +extension on PowerSyncDatabase { + Stream> get attachments { + return watch('SELECT * FROM attachments_queue') + .map((rs) => rs.map(Attachment.fromRow).toList()); + } +} + +final class MockRemoteStorage extends Mock implements RemoteStorage { + MockRemoteStorage() { + when(uploadFile(any, any)).thenAnswer((_) async {}); + when(downloadFile(any)).thenAnswer((_) async { + return Stream.empty(); + }); + when(deleteFile(any)).thenAnswer((_) async {}); + } + + @override + Future uploadFile( + Stream? fileData, Attachment? attachment) async { + await noSuchMethod(Invocation.method(#uploadFile, [fileData, attachment])); + } + + @override + Future>> downloadFile(Attachment? attachment) { + return (noSuchMethod(Invocation.method(#downloadFile, [attachment])) ?? + Future.value(const Stream>.empty())) + as Future>>; + } + + @override + Future deleteFile(Attachment? attachment) async { + await noSuchMethod(Invocation.method(#deleteFile, [attachment])); + } +} + +final _schema = Schema([ + Table('users', + [Column.text('name'), Column.text('email'), Column.text('photo_id')]), + AttachmentsQueueTable(), +]); + +TypeMatcher isAttachment(String id) { + return isA().having((e) => e.id, 'id', id); +} diff --git a/packages/powersync_core/test/attachments/local_storage_test.dart b/packages/powersync_core/test/attachments/local_storage_test.dart new file mode 100644 index 00000000..9ceabac1 --- /dev/null +++ b/packages/powersync_core/test/attachments/local_storage_test.dart @@ -0,0 +1,365 @@ +@TestOn('vm') +library; + +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:test/test.dart'; +import 'package:path/path.dart' as p; +import 'package:powersync_core/src/attachments/io_local_storage.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +void main() { + group('IOLocalStorage', () { + late IOLocalStorage storage; + + setUp(() async { + storage = IOLocalStorage(Directory(d.sandbox)); + }); + + tearDown(() async { + // Clean up is handled automatically by test_descriptor + // No manual cleanup needed + }); + + group('saveFile and readFile', () { + test('saves and reads binary data successfully', () async { + const filePath = 'test_file'; + final data = Uint8List.fromList([1, 2, 3, 4, 5]); + final size = await storage.saveFile(filePath, Stream.value(data)); + expect(size, equals(data.length)); + + final resultStream = storage.readFile(filePath); + final result = await resultStream.toList(); + expect(result, equals([data])); + + // Assert filesystem state using test_descriptor + await d.file(filePath, data).validate(); + }); + + test('throws when reading non-existent file', () async { + const filePath = 'non_existent'; + expect( + () => storage.readFile(filePath).toList(), + throwsA(isA()), + ); + + // Assert file does not exist using Dart's File API + expect(await File(p.join(d.sandbox, filePath)).exists(), isFalse); + }); + + test('creates parent directories if they do not exist', () async { + const filePath = 'subdir/nested/test'; + final nonExistentDir = Directory(p.join(d.sandbox, 'subdir', 'nested')); + final data = Uint8List.fromList([1, 2, 3]); + + expect(await nonExistentDir.exists(), isFalse); + + final size = await storage.saveFile(filePath, Stream.value(data)); + expect(size, equals(data.length)); + expect(await nonExistentDir.exists(), isTrue); + + final resultStream = storage.readFile(filePath); + final result = await resultStream.toList(); + expect(result, equals([data])); + + // Assert directory structure + await d.dir('subdir/nested', [d.file('test', data)]).validate(); + }); + + test('creates all parent directories for deeply nested file', () async { + const filePath = 'a/b/c/d/e/f/g/h/i/j/testfile'; + final nestedDir = Directory( + p.join(d.sandbox, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'), + ); + final data = Uint8List.fromList([42, 43, 44]); + + expect(await nestedDir.exists(), isFalse); + + final size = await storage.saveFile(filePath, Stream.value(data)); + expect(size, equals(data.length)); + expect(await nestedDir.exists(), isTrue); + + final resultStream = storage.readFile(filePath); + final result = await resultStream.toList(); + expect(result, equals([data])); + + // Assert deep directory structure + await d.dir('a/b/c/d/e/f/g/h/i/j', [ + d.file('testfile', data), + ]).validate(); + }); + + test('overwrites existing file', () async { + const filePath = 'overwrite_test'; + final originalData = Uint8List.fromList([1, 2, 3]); + final newData = Uint8List.fromList([4, 5, 6, 7]); + + await storage.saveFile(filePath, Stream.value(originalData)); + final size = await storage.saveFile(filePath, Stream.value(newData)); + expect(size, equals(newData.length)); + + final resultStream = storage.readFile(filePath); + final result = await resultStream.toList(); + expect(result, equals([newData])); + + // Assert file content + await d.file(filePath, newData).validate(); + }); + }); + + group('edge cases and robustness', () { + test('saveFile with empty data writes empty file and returns 0 size', + () async { + const filePath = 'empty_file'; + + final size = await storage.saveFile(filePath, Stream.empty()); + expect(size, 0); + + final resultStream = storage.readFile(filePath); + final chunks = await resultStream.toList(); + expect(chunks, isEmpty); + + final file = File(p.join(d.sandbox, filePath)); + expect(await file.exists(), isTrue); + expect(await file.length(), 0); + }); + + test('readFile preserves byte order (chunking may differ)', () async { + const filePath = 'ordered_chunks'; + final chunks = [ + Uint8List.fromList([0, 1, 2]), + Uint8List.fromList([3, 4]), + Uint8List.fromList([5, 6, 7, 8]), + ]; + final expectedBytes = + Uint8List.fromList(chunks.expand((c) => c).toList()); + await storage.saveFile(filePath, Stream.value(expectedBytes)); + + final outChunks = await storage.readFile(filePath).toList(); + final outBytes = Uint8List.fromList( + outChunks.expand((c) => c).toList(), + ); + expect(outBytes, equals(expectedBytes)); + }); + + test('fileExists becomes false after deleteFile', () async { + const filePath = 'exists_then_delete'; + await storage.saveFile(filePath, Stream.value(Uint8List.fromList([1]))); + expect(await storage.fileExists(filePath), isTrue); + await storage.deleteFile(filePath); + expect(await storage.fileExists(filePath), isFalse); + }); + + test('initialize is idempotent', () async { + await storage.initialize(); + await storage.initialize(); + + // Create a file, then re-initialize again + const filePath = 'idempotent_test'; + await storage.saveFile(filePath, Stream.value(Uint8List.fromList([9]))); + await storage.initialize(); + + // File should still exist (initialize should not clear data) + expect(await storage.fileExists(filePath), isTrue); + }); + + test('clear works even if base directory was removed externally', + () async { + await storage.initialize(); + + // Remove the base dir manually + final baseDir = Directory(d.sandbox); + if (await baseDir.exists()) { + await baseDir.delete(recursive: true); + } + + // Calling clear should recreate base dir + await storage.clear(); + expect(await baseDir.exists(), isTrue); + }); + + test('supports unicode and emoji filenames', () async { + const filePath = '測試_файл_📷.bin'; + final bytes = Uint8List.fromList([10, 20, 30, 40]); + await storage.saveFile(filePath, Stream.value(bytes)); + + final out = await storage.readFile(filePath).toList(); + expect(out, equals([bytes])); + + await d.file(filePath, bytes).validate(); + }); + + test('readFile accepts mediaType parameter (ignored by IO impl)', + () async { + const filePath = 'with_media_type'; + final data = Uint8List.fromList([1, 2, 3]); + await storage.saveFile(filePath, Stream.value(data)); + + final result = + await storage.readFile(filePath, mediaType: 'image/jpeg').toList(); + expect(result, equals([data])); + }); + }); + + group('deleteFile', () { + test('deletes existing file', () async { + const filePath = 'delete_test'; + final data = Uint8List.fromList([1, 2, 3]); + + await storage.saveFile(filePath, Stream.value(data)); + expect(await storage.fileExists(filePath), isTrue); + + await storage.deleteFile(filePath); + expect(await storage.fileExists(filePath), isFalse); + + // Assert file does not exist + expect(await File(p.join(d.sandbox, filePath)).exists(), isFalse); + }); + + test('does not throw when deleting non-existent file', () async { + const filePath = 'non_existent'; + await storage.deleteFile(filePath); + expect(await File(p.join(d.sandbox, filePath)).exists(), isFalse); + }); + }); + + group('initialize and clear', () { + test('initialize creates the base directory', () async { + final newStorage = + IOLocalStorage(Directory(p.join(d.sandbox, 'new_dir'))); + final baseDir = Directory(p.join(d.sandbox, 'new_dir')); + + expect(await baseDir.exists(), isFalse); + + await newStorage.initialize(); + + expect(await baseDir.exists(), isTrue); + }); + + test('clear removes and recreates the base directory', () async { + await storage.initialize(); + final testFile = p.join(d.sandbox, 'test_file'); + await File(testFile).writeAsString('test'); + + expect(await File(testFile).exists(), isTrue); + + await storage.clear(); + + expect(await Directory(d.sandbox).exists(), isTrue); + expect(await File(testFile).exists(), isFalse); + }); + }); + + group('fileExists', () { + test('returns true for existing file', () async { + const filePath = 'exists_test'; + final data = Uint8List.fromList([1, 2, 3]); + + await storage.saveFile(filePath, Stream.value(data)); + expect(await storage.fileExists(filePath), isTrue); + + await d.file(filePath, data).validate(); + }); + + test('returns false for non-existent file', () async { + const filePath = 'non_existent'; + expect(await storage.fileExists(filePath), isFalse); + expect(await File(p.join(d.sandbox, filePath)).exists(), isFalse); + }); + }); + + group('file system integration', () { + test('handles special characters in file path', () async { + const filePath = 'file with spaces & symbols!@#'; + final data = Uint8List.fromList([1, 2, 3]); + + final size = await storage.saveFile(filePath, Stream.value(data)); + expect(size, equals(data.length)); + + final resultStream = storage.readFile(filePath); + final result = await resultStream.toList(); + expect(result, equals([data])); + + await d.file(filePath, data).validate(); + }); + + test('handles large binary data stream', () async { + const filePath = 'large_file'; + final data = Uint8List.fromList(List.generate(10000, (i) => i % 256)); + final chunkSize = 1000; + final chunks = []; + for (var i = 0; i < data.length; i += chunkSize) { + chunks.add( + Uint8List.fromList( + data.sublist( + i, + i + chunkSize < data.length ? i + chunkSize : data.length, + ), + ), + ); + } + final size = await storage.saveFile(filePath, Stream.value(data)); + expect(size, equals(data.length)); + + final resultStream = storage.readFile(filePath); + final result = Uint8List.fromList( + (await resultStream.toList()).expand((chunk) => chunk).toList(), + ); + expect(result, equals(data)); + + await d.file(filePath, data).validate(); + }); + }); + + group('concurrent operations', () { + test('handles concurrent saves to different files', () async { + final futures = >[]; + final fileCount = 10; + + for (int i = 0; i < fileCount; i++) { + final data = Uint8List.fromList([i, i + 1, i + 2]); + futures.add(storage.saveFile('file_$i', Stream.value(data))); + } + + await Future.wait(futures); + + for (int i = 0; i < fileCount; i++) { + final resultStream = storage.readFile('file_$i'); + final result = await resultStream.toList(); + expect( + result, + equals([ + Uint8List.fromList([i, i + 1, i + 2]), + ]), + ); + await d + .file('file_$i', Uint8List.fromList([i, i + 1, i + 2])) + .validate(); + } + }); + + test('handles concurrent saves to the same file', () async { + const filePath = 'concurrent_test'; + final data1 = Uint8List.fromList([1, 2, 3]); + final data2 = Uint8List.fromList([4, 5, 6]); + final futures = [ + storage.saveFile(filePath, Stream.value(data1)), + storage.saveFile(filePath, Stream.value(data2)), + ]; + + await Future.wait(futures); + + final resultStream = storage.readFile(filePath); + final result = await resultStream.toList(); + expect(result, anyOf(equals([data1]), equals([data2]))); + + // Assert one of the possible outcomes + final file = File(p.join(d.sandbox, filePath)); + final fileData = await file.readAsBytes(); + expect(fileData, anyOf(equals(data1), equals(data2))); + }); + }); + }); +} diff --git a/packages/powersync_core/test/connected_test.dart b/packages/powersync_core/test/connected_test.dart new file mode 100644 index 00000000..a2188958 --- /dev/null +++ b/packages/powersync_core/test/connected_test.dart @@ -0,0 +1,248 @@ +@TestOn('!browser') +library; + +// This test uses a local server which is possible to control in Web via hybrid main, +// but this makes the test significantly more complex. +import 'dart:async'; + +import 'package:powersync_core/powersync_core.dart'; +import 'package:test/test.dart'; + +import 'server/sync_server/mock_sync_server.dart'; +import 'utils/abstract_test_utils.dart'; +import 'utils/test_utils_impl.dart'; + +final testUtils = TestUtils(); + +void main() { + group('connected tests', () { + late String path; + setUp(() async { + path = testUtils.dbPath(); + }); + + tearDown(() async { + await testUtils.cleanDb(path: path); + }); + + createTestServer() async { + final testServer = TestHttpServerHelper(); + await testServer.start(); + addTearDown(() => testServer.stop()); + return testServer; + } + + test('should connect to mock PowerSync instance', () async { + final testServer = await createTestServer(); + final connector = TestConnector(() async { + return PowerSyncCredentials( + endpoint: testServer.uri.toString(), + token: 'token not used here', + expiresAt: DateTime.now()); + }); + + final db = PowerSyncDatabase.withFactory( + await testUtils.testFactory(path: path), + schema: defaultSchema, + maxReaders: 3); + addTearDown(() => {db.close()}); + await db.initialize(); + + final connectedCompleter = Completer(); + + db.statusStream.listen((status) { + if (status.connected) { + connectedCompleter.complete(); + } + }); + + // Add a basic command for the test server to send + testServer.addEvent('{"token_expires_in": 3600}\n'); + + await db.connect(connector: connector); + await connectedCompleter.future; + + expect(db.connected, isTrue); + await db.disconnect(); + }); + + test('should trigger uploads when connection is re-established', () async { + int uploadCounter = 0; + var uploadTriggeredCompleter = Completer(); + final testServer = await createTestServer(); + final connector = TestConnector(() async { + return PowerSyncCredentials( + endpoint: testServer.uri.toString(), + token: 'token not used here', + expiresAt: DateTime.now()); + }, uploadData: (database) async { + uploadCounter++; + uploadTriggeredCompleter.complete(); + throw Exception('No uploads occur here'); + }); + + final db = PowerSyncDatabase.withFactory( + await testUtils.testFactory(path: path), + schema: defaultSchema, + maxReaders: 3); + // Shorter retry delay, to speed up tests + // ignore: deprecated_member_use_from_same_package + db.retryDelay = Duration(milliseconds: 10); + addTearDown(() => {db.close()}); + await db.initialize(); + + // Create an item which should trigger an upload. + await db.execute( + 'INSERT INTO customers (id, name) VALUES (uuid(), ?)', ['steven']); + + // Create a new completer to await the next upload + uploadTriggeredCompleter = Completer(); + + // Connect the PowerSync instance + final connectedCompleter = Completer(); + // The first connection attempt will fail + final connectedErroredCompleter = Completer(); + + db.statusStream.listen((status) { + if (status.connected && !connectedCompleter.isCompleted) { + connectedCompleter.complete(); + } + if (status.downloadError != null && + !connectedErroredCompleter.isCompleted) { + connectedErroredCompleter.complete(); + } + }); + + // The first command will not be valid, this simulates a failed connection + testServer.addEvent('asdf\n'); + await db.connect(connector: connector); + + // The connect operation should have triggered an upload (even though it fails to connect) + await uploadTriggeredCompleter.future; + expect(uploadCounter, equals(1)); + // Create a new completer for the next iteration + uploadTriggeredCompleter = Completer(); + + // Connection attempt should initially fail + await connectedErroredCompleter.future; + expect(db.currentStatus.anyError, isNotNull); + + // Now send a valid command. Which will result in successful connection + await testServer.clearEvents(); + testServer.addEvent('{"token_expires_in": 3600}\n'); + await connectedCompleter.future; + expect(db.connected, isTrue); + + await uploadTriggeredCompleter.future; + expect(uploadCounter, equals(2)); + + await db.disconnect(); + }); + + test('should persist local changes when there is no write checkpoint', + () async { + final testServer = await createTestServer(); + final connector = TestConnector(() async { + return PowerSyncCredentials( + endpoint: testServer.uri.toString(), + token: 'token not used here', + expiresAt: DateTime.now()); + }, uploadData: (database) async { + final tx = await database.getNextCrudTransaction(); + if (tx != null) { + await tx.complete(); + } + }); + + final db = PowerSyncDatabase.withFactory( + await testUtils.testFactory(path: path), + schema: defaultSchema, + maxReaders: 3); + addTearDown(() => {db.close()}); + await db.initialize(); + + // Create an item which should trigger an upload. + await db.execute( + 'INSERT INTO customers (id, name) VALUES (uuid(), ?)', ['steven']); + + // Manually simulate upload before connecting. + // This is simpler than doing this via connect() and waiting for it to complete. + await connector.uploadData(db); + + // Check that the data is present locally + expect( + await db.getAll('select name from customers'), + equals([ + {'name': 'steven'} + ])); + + // Connect and send a checkpoint back, but no write checkpoint. + testServer + .addEvent('{"checkpoint": {"last_op_id": "10", "buckets": []}}\n'); + testServer.addEvent('{"checkpoint_complete": {"last_op_id": "10"}}\n'); + + // Now connect and wait for sync to complete + await db.connect(connector: connector); + await db.statusStream + .firstWhere((status) => status.connected && status.downloading); + await Future.delayed(Duration(milliseconds: 20)); + expect( + await db.getAll('select name from customers'), + equals([ + {'name': 'steven'} + ])); + }); + + test('should remove local changes when there a write checkpoint', () async { + // The only difference between this and the one above, is that the synced + // checkpoint here contains a write checkpoint, matching the write-checkpoint2.json + // API. This will trigger the local changes to be removed. + final testServer = await createTestServer(); + final connector = TestConnector(() async { + return PowerSyncCredentials( + endpoint: testServer.uri.toString(), + token: 'token not used here', + expiresAt: DateTime.now()); + }, uploadData: (database) async { + final tx = await database.getNextCrudTransaction(); + if (tx != null) { + await tx.complete(); + } + }); + + final db = PowerSyncDatabase.withFactory( + await testUtils.testFactory(path: path), + schema: defaultSchema, + maxReaders: 3); + addTearDown(() => {db.close()}); + await db.initialize(); + + // Create an item which should trigger an upload. + await db.execute( + 'INSERT INTO customers (id, name) VALUES (uuid(), ?)', ['steven']); + + // Manually simulate upload before connecting. + // This is simpler than doing this via connect() and waiting for it to complete. + await connector.uploadData(db); + + // Check that the data is present locally + expect( + await db.getAll('select name from customers'), + equals([ + {'name': 'steven'} + ])); + + // Connect and send a checkpoint back, but no write checkpoint. + testServer.addEvent( + '{"checkpoint": {"last_op_id": "10", "buckets": [], "write_checkpoint": "10"}}\n'); + testServer.addEvent('{"checkpoint_complete": {"last_op_id": "10"}}\n'); + + // Now connect and wait for sync to complete + await db.connect(connector: connector); + await db.statusStream + .firstWhere((status) => status.connected && status.downloading); + await Future.delayed(Duration(milliseconds: 20)); + expect(await db.getAll('select name from customers'), equals([])); + }); + }); +} diff --git a/packages/powersync/test/credentials_test.dart b/packages/powersync_core/test/credentials_test.dart similarity index 90% rename from packages/powersync/test/credentials_test.dart rename to packages/powersync_core/test/credentials_test.dart index fcdb2551..013b5efe 100644 --- a/packages/powersync/test/credentials_test.dart +++ b/packages/powersync_core/test/credentials_test.dart @@ -1,4 +1,4 @@ -import 'package:powersync/powersync.dart'; +import 'package:powersync_core/powersync_core.dart'; import 'package:test/test.dart'; void main() { diff --git a/packages/powersync/test/crud_test.dart b/packages/powersync_core/test/crud_test.dart similarity index 64% rename from packages/powersync/test/crud_test.dart rename to packages/powersync_core/test/crud_test.dart index ba7de5c7..0e188075 100644 --- a/packages/powersync/test/crud_test.dart +++ b/packages/powersync_core/test/crud_test.dart @@ -1,4 +1,4 @@ -import 'package:powersync/powersync.dart'; +import 'package:powersync_core/powersync_core.dart'; import 'package:sqlite_async/sqlite3_common.dart'; import 'package:test/test.dart'; @@ -29,7 +29,7 @@ void main() { equals([ { 'data': - '{"op":"PUT","type":"assets","id":"$testId","data":{"description":"test"}}' + '{"op":"PUT","id":"$testId","type":"assets","data":{"description":"test"}}' } ])); @@ -59,7 +59,7 @@ void main() { equals([ { 'data': - '{"op":"PUT","type":"assets","id":"$testId","data":{"description":"test2"}}' + '{"op":"PUT","id":"$testId","type":"assets","data":{"description":"test2"}}' } ])); @@ -72,7 +72,7 @@ void main() { 'INSERT INTO assets(id, description) VALUES(?, ?)', [testId, 'test3']); }, - throwsA((e) => + throwsA((dynamic e) => e is SqliteException && e.message.contains('UNIQUE constraint failed'))); }); @@ -91,7 +91,7 @@ void main() { equals([ { 'data': - '{"op":"PATCH","type":"assets","id":"$testId","data":{"description":"test2"}}' + '{"op":"PATCH","id":"$testId","type":"assets","data":{"description":"test2"}}' } ])); @@ -116,7 +116,7 @@ void main() { expect( await powersync.getAll('SELECT data FROM ps_crud ORDER BY id'), equals([ - {'data': '{"op":"DELETE","type":"assets","id":"$testId"}'} + {'data': '{"op":"DELETE","id":"$testId","type":"assets"}'} ])); var tx = (await powersync.getNextCrudTransaction())!; @@ -132,13 +132,14 @@ void main() { 'INSERT INTO assets(id, description) VALUES(?, ?) ON CONFLICT DO UPDATE SET description = ?', [testId, 'test2', 'test3']); }, - throwsA((e) => + throwsA((dynamic e) => e is SqliteException && e.message.contains('cannot UPSERT a view'))); }); test('INSERT-only tables', () async { await powersync.disconnectAndClear(); + await powersync.close(); powersync = await testUtils.setupPowerSync( path: path, schema: const Schema([ @@ -212,7 +213,7 @@ void main() { equals([ { 'data': - '{"op":"PUT","type":"assets","id":"$testId","data":{"quantity":"$bigNumber"}}' + '{"op":"PUT","id":"$testId","type":"assets","data":{"quantity":"$bigNumber"}}' } ])); @@ -226,7 +227,7 @@ void main() { equals([ { 'data': - '{"op":"PATCH","type":"assets","id":"$testId","data":{"quantity":${bigNumber + 1}}}' + '{"op":"PATCH","id":"$testId","type":"assets","data":{"quantity":${bigNumber + 1}}}' } ])); }); @@ -269,5 +270,132 @@ void main() { await tx2.complete(); expect(await powersync.getNextCrudTransaction(), equals(null)); }); + + test('nextCrudTransactions', () async { + Future createTransaction(int size) { + return powersync.writeTransaction((tx) async { + for (var i = 0; i < size; i++) { + await tx.execute('INSERT INTO assets (id) VALUES (uuid())'); + } + }); + } + + await expectLater(powersync.getCrudTransactions(), emitsDone); + + await createTransaction(5); + await createTransaction(10); + await createTransaction(15); + + CrudTransaction? lastTransaction; + final batch = []; + await for (final transaction in powersync.getCrudTransactions()) { + batch.addAll(transaction.crud); + lastTransaction = transaction; + + if (batch.length > 10) { + break; + } + } + + expect(batch, hasLength(15)); + await lastTransaction!.complete(); + + final remainingTransaction = await powersync.getNextCrudTransaction(); + expect(remainingTransaction?.crud, hasLength(15)); + }); + + test('include metadata', () async { + await powersync.updateSchema(Schema([ + Table( + 'lists', + [Column.text('name')], + trackMetadata: true, + ) + ])); + + await powersync.execute( + 'INSERT INTO lists (id, name, _metadata) VALUES (uuid(), ?, ?)', + ['entry', 'so meta']); + + final batch = await powersync.getNextCrudTransaction(); + expect(batch!.crud[0].metadata, 'so meta'); + }); + + test('include old values', () async { + await powersync.updateSchema(Schema([ + Table( + 'lists', + [Column.text('name'), Column.text('content')], + trackPreviousValues: TrackPreviousValuesOptions(), + ) + ])); + + await powersync.execute( + 'INSERT INTO lists (id, name, content) VALUES (uuid(), ?, ?)', + ['entry', 'content']); + await powersync.execute('DELETE FROM ps_crud;'); + await powersync.execute('UPDATE lists SET name = ?;', ['new name']); + + final batch = await powersync.getNextCrudTransaction(); + expect(batch!.crud[0].previousValues, + {'name': 'entry', 'content': 'content'}); + }); + + test('include old values with column filter', () async { + await powersync.updateSchema(Schema([ + Table( + 'lists', + [Column.text('name'), Column.text('content')], + trackPreviousValues: + TrackPreviousValuesOptions(columnFilter: ['name']), + ) + ])); + + await powersync.execute( + 'INSERT INTO lists (id, name, content) VALUES (uuid(), ?, ?)', + ['name', 'content']); + await powersync.execute('DELETE FROM ps_crud;'); + await powersync.execute('UPDATE lists SET name = ?, content = ?', + ['new name', 'new content']); + + final batch = await powersync.getNextCrudTransaction(); + expect(batch!.crud[0].previousValues, {'name': 'name'}); + }); + + test('include old values when changed', () async { + await powersync.updateSchema(Schema([ + Table( + 'lists', + [Column.text('name'), Column.text('content')], + trackPreviousValues: + TrackPreviousValuesOptions(onlyWhenChanged: true), + ) + ])); + + await powersync.execute( + 'INSERT INTO lists (id, name, content) VALUES (uuid(), ?, ?)', + ['name', 'content']); + await powersync.execute('DELETE FROM ps_crud;'); + await powersync.execute('UPDATE lists SET name = ?', ['new name']); + + final batch = await powersync.getNextCrudTransaction(); + expect(batch!.crud[0].previousValues, {'name': 'name'}); + }); + + test('ignore empty update', () async { + await powersync.updateSchema(Schema([ + Table( + 'lists', + [Column.text('name')], + ignoreEmptyUpdates: true, + ) + ])); + + await powersync + .execute('INSERT INTO lists (id, name) VALUES (uuid(), ?)', ['name']); + await powersync.execute('DELETE FROM ps_crud;'); + await powersync.execute('UPDATE lists SET name = ?;', ['name']); + expect(await powersync.getNextCrudTransaction(), isNull); + }); }); } diff --git a/packages/powersync_core/test/database/core_version_test.dart b/packages/powersync_core/test/database/core_version_test.dart new file mode 100644 index 00000000..84827189 --- /dev/null +++ b/packages/powersync_core/test/database/core_version_test.dart @@ -0,0 +1,39 @@ +import 'package:powersync_core/src/database/core_version.dart'; +import 'package:sqlite3/common.dart'; +import 'package:test/test.dart'; + +void main() { + group('PowerSyncCoreVersion', () { + test('parse', () { + expect(PowerSyncCoreVersion.parse('0.3.9/5d64f366'), (0, 3, 9)); + }); + + test('compare', () { + void expectLess(String a, String b) { + final parsedA = PowerSyncCoreVersion.parse(a); + final parsedB = PowerSyncCoreVersion.parse(b); + + expect(parsedA.compareTo(parsedB), -1); + expect(parsedB.compareTo(parsedA), 1); + + expect(parsedA.compareTo(parsedA), 0); + expect(parsedB.compareTo(parsedB), 0); + } + + expectLess('0.1.0', '1.0.0'); + expectLess('1.0.0', '1.2.0'); + expectLess('0.3.9', '0.3.11'); + }); + + test('checkSupported', () { + expect(PowerSyncCoreVersion.parse('0.3.10').checkSupported, + throwsA(isA())); + expect(PowerSyncCoreVersion.parse('1.0.0').checkSupported, + throwsA(isA())); + + PowerSyncCoreVersion.minimum.checkSupported(); + expect(PowerSyncCoreVersion.maximumExclusive.checkSupported, + throwsA(isA())); + }); + }); +} diff --git a/packages/powersync/test/disconnect_test.dart b/packages/powersync_core/test/disconnect_test.dart similarity index 91% rename from packages/powersync/test/disconnect_test.dart rename to packages/powersync_core/test/disconnect_test.dart index 80cb52ed..86f18130 100644 --- a/packages/powersync/test/disconnect_test.dart +++ b/packages/powersync_core/test/disconnect_test.dart @@ -1,7 +1,7 @@ -import 'package:powersync/powersync.dart'; -import 'package:powersync/sqlite_async.dart'; +import 'package:powersync_core/powersync_core.dart'; +import 'package:powersync_core/sqlite_async.dart'; import 'package:test/test.dart'; -import 'streaming_sync_test.dart'; +import 'utils/abstract_test_utils.dart'; import 'utils/test_utils_impl.dart'; import 'watch_test.dart'; @@ -29,6 +29,7 @@ void main() { expiresAt: DateTime.now()); } + // ignore: deprecated_member_use_from_same_package db.retryDelay = Duration(milliseconds: 5000); var connector = TestConnector(credentialsCallback); await db.connect(connector: connector); diff --git a/packages/powersync_core/test/exceptions_test.dart b/packages/powersync_core/test/exceptions_test.dart new file mode 100644 index 00000000..d63a152d --- /dev/null +++ b/packages/powersync_core/test/exceptions_test.dart @@ -0,0 +1,56 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:powersync_core/src/exceptions.dart'; +import 'package:test/test.dart'; + +void main() { + group('SyncResponseException', () { + const errorResponse = + '{"error":{"code":"PSYNC_S2106","status":401,"description":"Authentication required","name":"AuthorizationError"}}'; + + test('fromStreamedResponse', () async { + final exc = await SyncResponseException.fromStreamedResponse( + StreamedResponse(Stream.value(utf8.encode(errorResponse)), 401)); + + expect(exc.statusCode, 401); + expect(exc.description, + 'Request failed: PSYNC_S2106(AuthorizationError): Authentication required'); + }); + + test('fromResponse', () { + final exc = + SyncResponseException.fromResponse(Response(errorResponse, 401)); + expect(exc.statusCode, 401); + expect(exc.description, + 'Request failed: PSYNC_S2106(AuthorizationError): Authentication required'); + }); + + test('with description', () { + const errorResponse = + '{"error":{"code":"PSYNC_S2106","status":401,"description":"Authentication required","name":"AuthorizationError", "details": "Missing authorization header"}}'; + + final exc = + SyncResponseException.fromResponse(Response(errorResponse, 401)); + expect(exc.statusCode, 401); + expect(exc.description, + 'Request failed: PSYNC_S2106(AuthorizationError): Authentication required, Missing authorization header'); + }); + + test('malformed', () { + const malformed = + '{"message":"Route GET:/foo/bar not found","error":"Not Found","statusCode":404}'; + + final exc = SyncResponseException.fromResponse(Response(malformed, 401)); + expect(exc.statusCode, 401); + expect(exc.description, + 'Request failed: {"message":"Route GET:/foo/bar not found","error":"Not Found","statusCode":404}'); + + final exc2 = SyncResponseException.fromResponse(Response( + 'not even json', 500, + reasonPhrase: 'Internal server error')); + expect(exc2.statusCode, 500); + expect(exc2.description, 'Internal server error'); + }); + }); +} diff --git a/packages/powersync/test/offline_online_test.dart b/packages/powersync_core/test/offline_online_test.dart similarity index 97% rename from packages/powersync/test/offline_online_test.dart rename to packages/powersync_core/test/offline_online_test.dart index 8c831806..b51766a6 100644 --- a/packages/powersync/test/offline_online_test.dart +++ b/packages/powersync_core/test/offline_online_test.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:powersync/powersync.dart'; +import 'package:powersync_core/powersync_core.dart'; import 'package:sqlite_async/src/utils/shared_utils.dart'; import 'package:test/test.dart'; @@ -118,7 +118,7 @@ void main() { }); final crud = (await db.getAll('SELECT data FROM ps_crud ORDER BY id')) - .map((d) => jsonDecode(d['data'])) + .map((d) => jsonDecode(d['data'] as String)) .toList(); expect( crud, diff --git a/packages/powersync/test/performance_native_test.dart b/packages/powersync_core/test/performance_native_test.dart similarity index 98% rename from packages/powersync/test/performance_native_test.dart rename to packages/powersync_core/test/performance_native_test.dart index 9def5945..66117751 100644 --- a/packages/powersync/test/performance_native_test.dart +++ b/packages/powersync_core/test/performance_native_test.dart @@ -1,5 +1,7 @@ @TestOn('!browser') -import 'package:powersync/powersync.dart'; +library; + +import 'package:powersync_core/powersync_core.dart'; import 'package:test/test.dart'; import 'performance_shared_test.dart'; diff --git a/packages/powersync/test/performance_shared_test.dart b/packages/powersync_core/test/performance_shared_test.dart similarity index 97% rename from packages/powersync/test/performance_shared_test.dart rename to packages/powersync_core/test/performance_shared_test.dart index 5044c099..1d5222f3 100644 --- a/packages/powersync/test/performance_shared_test.dart +++ b/packages/powersync_core/test/performance_shared_test.dart @@ -1,4 +1,4 @@ -import 'package:powersync/powersync.dart'; +import 'package:powersync_core/powersync_core.dart'; import 'package:test/test.dart'; import 'utils/test_utils_impl.dart'; @@ -77,7 +77,7 @@ void main() { final timer = Stopwatch()..start(); await db.writeTransaction((tx) async { - List futures = []; + List> futures = []; for (var i = 0; i < 1000; i++) { var future = tx.execute( 'INSERT INTO customers(id, name, email) VALUES(uuid(), ?, ?)', diff --git a/packages/powersync/test/powersync_native_test.dart b/packages/powersync_core/test/powersync_native_test.dart similarity index 96% rename from packages/powersync/test/powersync_native_test.dart rename to packages/powersync_core/test/powersync_native_test.dart index aeda23d8..0be588bf 100644 --- a/packages/powersync/test/powersync_native_test.dart +++ b/packages/powersync_core/test/powersync_native_test.dart @@ -1,8 +1,10 @@ @TestOn('!browser') +library; + import 'dart:async'; import 'dart:math'; -import 'package:powersync/powersync.dart'; +import 'package:powersync_core/powersync_core.dart'; import 'package:sqlite_async/sqlite3_common.dart'; import 'package:test/test.dart'; import 'utils/abstract_test_utils.dart'; @@ -68,7 +70,7 @@ void main() { await expectLater(() async { await db.getAll('INSERT INTO assets(id) VALUES(?)', ['test']); }, - throwsA((e) => + throwsA((dynamic e) => e is SqliteException && e.message .contains('attempt to write in a read-only transaction'))); @@ -81,7 +83,7 @@ void main() { await db.getAll( "WITH test AS (SELECT 1 AS one) INSERT INTO assets(id) SELECT one FROM test"); }, - throwsA((e) => + throwsA((dynamic e) => e is SqliteException && e.message .contains('attempt to write in a read-only transaction'))); diff --git a/packages/powersync/test/powersync_shared_test.dart b/packages/powersync_core/test/powersync_shared_test.dart similarity index 53% rename from packages/powersync/test/powersync_shared_test.dart rename to packages/powersync_core/test/powersync_shared_test.dart index 26b458d8..b4317d89 100644 --- a/packages/powersync/test/powersync_shared_test.dart +++ b/packages/powersync_core/test/powersync_shared_test.dart @@ -1,3 +1,5 @@ +import 'package:logging/logging.dart'; +import 'package:powersync_core/powersync_core.dart'; import 'package:sqlite_async/mutex.dart'; import 'package:test/test.dart'; import 'package:uuid/parsing.dart'; @@ -19,6 +21,33 @@ void main() { await testUtils.cleanDb(path: path); }); + test('warns on duplicate database', () async { + final logger = Logger.detached('powersync.test')..level = Level.WARNING; + final events = []; + final subscription = logger.onRecord.listen(events.add); + addTearDown(subscription.cancel); + + final firstInstance = + await testUtils.setupPowerSync(path: path, logger: logger); + await firstInstance.initialize(); + expect(events, isEmpty); + + final secondInstance = + await testUtils.setupPowerSync(path: path, logger: logger); + await secondInstance.initialize(); + expect( + events, + contains( + isA().having( + (e) => e.message, + 'message', + contains( + 'Multiple instances for the same database have been detected.'), + ), + ), + ); + }); + test('should not allow direct db calls within a transaction callback', () async { final db = await testUtils.setupPowerSync(path: path); @@ -26,7 +55,9 @@ void main() { await db.writeTransaction((tx) async { await expectLater(() async { await db.execute('INSERT INTO assets(id) VALUES(?)', ['test']); - }, throwsA((e) => e is LockError && e.message.contains('tx.execute'))); + }, + throwsA((dynamic e) => + e is LockError && e.message.contains('tx.execute'))); }); }); @@ -40,7 +71,9 @@ void main() { // allow it by default. await expectLater(() async { await db.getAll('SELECT * FROM assets'); - }, throwsA((e) => e is LockError && e.message.contains('tx.getAll'))); + }, + throwsA((dynamic e) => + e is LockError && e.message.contains('tx.getAll'))); }); await db.readTransaction((tx) async { @@ -50,7 +83,9 @@ void main() { // opens another connection, but doesn't use it. await expectLater(() async { await db.getAll('SELECT * FROM assets'); - }, throwsA((e) => e is LockError && e.message.contains('tx.getAll'))); + }, + throwsA((dynamic e) => + e is LockError && e.message.contains('tx.getAll'))); }); }); @@ -62,16 +97,16 @@ void main() { await expectLater(() async { await db.getOptional('SELECT * FROM assets'); }, - throwsA( - (e) => e is LockError && e.message.contains('tx.getOptional'))); + throwsA((dynamic e) => + e is LockError && e.message.contains('tx.getOptional'))); }); await db.readLock((tx) async { await expectLater(() async { await db.getOptional('SELECT * FROM assets'); }, - throwsA( - (e) => e is LockError && e.message.contains('tx.getOptional'))); + throwsA((dynamic e) => + e is LockError && e.message.contains('tx.getOptional'))); }); }); @@ -89,5 +124,27 @@ void main() { // Check that it is a valid uuid UuidParsing.parseAsByteList(id); }); + + test('does not emit duplicate sync status events', () async { + final db = await testUtils.setupPowerSync(path: path); + expectLater( + db.statusStream, + emitsInOrder( + [ + // Manual setStatus call. hasSynced set to true because lastSyncedAt is set + isA().having((e) => e.hasSynced, 'hasSynced', true), + // Closing the database emits a disconnected status + isA().having((e) => e.connected, 'connected', false), + emitsDone + ], + ), + ); + + final status = SyncStatus(connected: true, lastSyncedAt: DateTime.now()); + db.setStatus(status); + db.setStatus(status); // Should not re-emit! + + await db.close(); + }); }); } diff --git a/packages/powersync/test/schema_test.dart b/packages/powersync_core/test/schema_test.dart similarity index 79% rename from packages/powersync/test/schema_test.dart rename to packages/powersync_core/test/schema_test.dart index 7dfda31c..f397d5bb 100644 --- a/packages/powersync/test/schema_test.dart +++ b/packages/powersync_core/test/schema_test.dart @@ -1,4 +1,4 @@ -import 'package:powersync/powersync.dart'; +import 'package:powersync_core/powersync_core.dart'; import 'package:test/test.dart'; import 'utils/test_utils_impl.dart'; @@ -76,7 +76,7 @@ void main() { // Updated expect(versionAfter2['schema_version'], - greaterThan(versionAfter['schema_version'])); + greaterThan(versionAfter['schema_version'] as int)); final schema3 = Schema([ Table('assets', [ @@ -106,7 +106,7 @@ void main() { // Updated again (index) expect(versionAfter3['schema_version'], - greaterThan(versionAfter2['schema_version'])); + greaterThan(versionAfter2['schema_version'] as int)); }); /// The assets table is locked after performing the EXPLAIN QUERY @@ -176,14 +176,11 @@ void main() { ]), ]); - try { - powersync.updateSchema(schema2); - } catch (e) { - expect( - e, - isA().having((e) => e.message, 'message', - 'Invalid characters in table name: #notworking')); - } + await expectLater( + () => powersync.updateSchema(schema2), + throwsA(isA().having((e) => e.message, 'message', + 'Invalid characters in table name: #notworking')), + ); }); }); @@ -303,7 +300,7 @@ void main() { test('Table with too many columns', () { final List manyColumns = List.generate( - 64, // Exceeds MAX_NUMBER_OF_COLUMNS + 2000, // Exceeds MAX_NUMBER_OF_COLUMNS (index) => Column('col$index', ColumnType.text), ); @@ -315,12 +312,32 @@ void main() { isA().having( (e) => e.message, 'message', - 'Table too_many_columns has more than 63 columns, which is not supported', + 'Table too_many_columns has more than 1999 columns, which is not supported', ), ), ); }); + test('local-only with metadata', () { + final table = Table('foo', [Column.text('bar')], + localOnly: true, trackMetadata: true); + + expect( + table.validate, + throwsA(isA().having((e) => e.message, 'emssage', + "Local-only tables can't track metadata"))); + }); + + test('local-only with trackPreviousValues', () { + final table = Table('foo', [Column.text('bar')], + localOnly: true, trackPreviousValues: TrackPreviousValuesOptions()); + + expect( + table.validate, + throwsA(isA().having((e) => e.message, 'emssage', + "Local-only tables can't track old values"))); + }); + test('Schema without duplicate table names', () { final schema = Schema([ Table('duplicate', [ @@ -365,13 +382,55 @@ void main() { ]); final json = table.toJson(); + expect(json, { + 'name': 'users', + 'view_name': null, + 'local_only': false, + 'insert_only': false, + 'columns': hasLength(2), + 'indexes': hasLength(1), + 'ignore_empty_update': false, + 'include_metadata': false, + }); + }); + + test('handles options', () { + expect(Table('foo', [], trackMetadata: true).toJson(), + containsPair('include_metadata', isTrue)); + + expect(Table('foo', [], ignoreEmptyUpdates: true).toJson(), + containsPair('ignore_empty_update', isTrue)); + + expect( + Table('foo', [], trackPreviousValues: TrackPreviousValuesOptions()) + .toJson(), + allOf( + containsPair('include_old', isTrue), + containsPair('include_old_only_when_changed', isFalse), + ), + ); - expect(json['name'], equals('users')); - expect(json['view_name'], isNull); - expect(json['local_only'], isFalse); - expect(json['insert_only'], isFalse); - expect(json['columns'].length, equals(2)); - expect(json['indexes'].length, equals(1)); + expect( + Table('foo', [], + trackPreviousValues: + TrackPreviousValuesOptions(columnFilter: ['foo', 'bar'])) + .toJson(), + allOf( + containsPair('include_old', ['foo', 'bar']), + containsPair('include_old_only_when_changed', isFalse), + ), + ); + + expect( + Table('foo', [], + trackPreviousValues: + TrackPreviousValuesOptions(onlyWhenChanged: true)) + .toJson(), + allOf( + containsPair('include_old', isTrue), + containsPair('include_old_only_when_changed', isTrue), + ), + ); }); }); } diff --git a/packages/powersync/test/server/asset_server.dart b/packages/powersync_core/test/server/asset_server.dart similarity index 100% rename from packages/powersync/test/server/asset_server.dart rename to packages/powersync_core/test/server/asset_server.dart diff --git a/packages/powersync_core/test/server/sync_server/in_memory_sync_server.dart b/packages/powersync_core/test/server/sync_server/in_memory_sync_server.dart new file mode 100644 index 00000000..278f014e --- /dev/null +++ b/packages/powersync_core/test/server/sync_server/in_memory_sync_server.dart @@ -0,0 +1,101 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:bson/bson.dart'; +import 'package:shelf/shelf.dart'; +import 'package:shelf_router/shelf_router.dart'; + +final class MockSyncService { + final bool useBson; + + // Use a queued stream to make tests easier. + StreamController controller = + StreamController(); + Completer _listener = Completer(); + + var router = Router(); + Object? Function() writeCheckpoint = () { + return { + 'data': {'write_checkpoint': '10'} + }; + }; + + MockSyncService({this.useBson = false}) { + router + ..post('/sync/stream', (Request request) async { + if (useBson && + !request.headers['Accept']! + .contains('application/vnd.powersync.bson-stream')) { + throw "Want to serve bson, but client doesn't accept it"; + } + + _listener.complete(request); + // Respond immediately with a stream + final bytes = controller.stream.map((line) { + return switch (line) { + final String line => utf8.encode(line), + final Uint8List line => line, + _ => throw ArgumentError.value(line, 'line', 'Unexpected type'), + }; + }); + + return Response.ok(bytes, headers: { + 'Content-Type': useBson + ? 'application/vnd.powersync.bson-stream' + : 'application/x-ndjson', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }, context: { + "shelf.io.buffer_output": false + }); + }) + ..get('/write-checkpoint2.json', (request) { + return Response.ok(json.encode(writeCheckpoint()), headers: { + 'Content-Type': 'application/json', + }); + }); + } + + Future get waitForListener => _listener.future; + + // Queue events which will be sent to connected clients. + void addRawEvent(Object data) { + controller.add(data); + } + + void addLine(Object? message) { + if (useBson) { + // Going through a JSON roundtrip ensures that the message can be + // serialized with the BSON package. + final cleanedMessage = json.decode(json.encode(message)); + addRawEvent(BsonCodec.serialize(cleanedMessage).byteList); + } else { + addRawEvent('${json.encode(message)}\n'); + } + } + + void addKeepAlive([int tokenExpiresIn = 3600]) { + addLine({'token_expires_in': tokenExpiresIn}); + } + + void endCurrentListener() { + controller.close(); + controller = StreamController(); + _listener = Completer(); + } + + // Clear events. We rely on a buffered controller here. Create a new controller + // in order to clear the buffer. + Future clearEvents() async { + await controller.close(); + _listener = Completer(); + controller = StreamController(); + } + + Future stop() async { + if (controller.hasListener) { + await controller.close(); + } + } +} diff --git a/packages/powersync_core/test/server/sync_server/mock_sync_server.dart b/packages/powersync_core/test/server/sync_server/mock_sync_server.dart new file mode 100644 index 00000000..e4710f3d --- /dev/null +++ b/packages/powersync_core/test/server/sync_server/mock_sync_server.dart @@ -0,0 +1,37 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:shelf/shelf_io.dart' as io; + +import 'in_memory_sync_server.dart'; + +// A basic Mock PowerSync service server which queues commands +// which clients can receive via connecting to the `/sync/stream` route. +// This assumes only one client will ever be connected at a time. +class TestHttpServerHelper { + final MockSyncService service = MockSyncService(); + late HttpServer _server; + + Uri get uri => Uri.parse('http://localhost:${_server.port}'); + + Future start() async { + _server = await io.serve(service.router.call, 'localhost', 0); + print('Test server running at ${_server.address}:${_server.port}'); + } + + // Queue events which will be sent to connected clients. + void addEvent(String data) { + service.addRawEvent(data); + } + + // Clear events. We rely on a buffered controller here. Create a new controller + // in order to clear the buffer. + Future clearEvents() async { + await service.clearEvents(); + } + + Future stop() async { + await service.stop(); + await _server.close(); + } +} diff --git a/packages/powersync/test/server/worker_server.dart b/packages/powersync_core/test/server/worker_server.dart similarity index 82% rename from packages/powersync/test/server/worker_server.dart rename to packages/powersync_core/test/server/worker_server.dart index 2889b1d5..9ff43323 100644 --- a/packages/powersync/test/server/worker_server.dart +++ b/packages/powersync_core/test/server/worker_server.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:dcli/dcli.dart'; import 'package:path/path.dart' as p; import 'package:shelf/shelf.dart'; import 'package:shelf/shelf_io.dart' as io; @@ -9,22 +8,21 @@ import 'package:stream_channel/stream_channel.dart'; import 'asset_server.dart'; Future hybridMain(StreamChannel channel) async { - final assetsDirectory = p - .normalize(p.join(DartScript.self.pathToScriptDirectory, '../../assets')); + final assetsDirectory = p.normalize('assets'); // Copy sqlite3.wasm file expected by the worker final sqliteOutputPath = p.join(assetsDirectory, 'sqlite3.wasm'); if (!(await File(sqliteOutputPath).exists())) { throw AssertionError( - 'sqlite3.wasm file should be present in the powersync/assets folder'); + 'sqlite3.wasm file should be present in the powersync_core/assets folder'); } final workerOutputPath = p.join(assetsDirectory, 'powersync_db.worker.js'); if (!(await File(workerOutputPath).exists())) { throw AssertionError( - 'powersync_db.worker.js file should be present in the powersync/assets folder'); + 'powersync_db.worker.js file should be present in the powersync_core/assets folder'); } final server = await HttpServer.bind('localhost', 0); diff --git a/packages/powersync/test/stream_test.dart b/packages/powersync_core/test/stream_test.dart similarity index 86% rename from packages/powersync/test/stream_test.dart rename to packages/powersync_core/test/stream_test.dart index 412711f4..56521333 100644 --- a/packages/powersync/test/stream_test.dart +++ b/packages/powersync_core/test/stream_test.dart @@ -1,10 +1,13 @@ @TestOn('!browser') +library; + import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:async/async.dart'; import 'package:http/http.dart'; -import 'package:powersync/src/stream_utils.dart'; +import 'package:powersync_core/src/sync/stream_utils.dart'; import 'package:test/test.dart'; void main() { @@ -17,7 +20,7 @@ void main() { [int count = 50, Object? error]) async* { for (var i = 0; i < count; i++) { yield "$prefix $i"; - await Future.delayed(delay); + await Future.delayed(delay); } if (error != null) { throw error; @@ -68,7 +71,7 @@ void main() { Stream stream2 = genStream('S2:', Duration(milliseconds: 20)).asBroadcastStream(); - var controller = StreamController(); + var controller = StreamController(); var stream1 = controller.stream; var merged = addBroadcast(stream1, stream2); @@ -129,11 +132,29 @@ void main() { expect(countS2, greaterThanOrEqualTo(0)); }); + test('addBroadcast - done', () async { + final a = StreamController(); + final b = StreamController.broadcast(); + + final stream = StreamQueue(addBroadcast(a.stream, b.stream)); + a.add('a1'); + await expectLater(stream, emits('a1')); + + expect(a.hasListener, isTrue); + expect(b.hasListener, isTrue); + + b.close(); + await expectLater(stream, emitsDone); + + expect(a.hasListener, isFalse); + expect(b.hasListener, isFalse); + }); + test('ndjson', () async { var sourceData = '{"line": 1}\n{"line": 2}\n'; var sourceBytes = Utf8Codec().encode(sourceData); var sourceStream = ByteStream.fromBytes(sourceBytes); - var parsedStream = ndjson(sourceStream); + var parsedStream = sourceStream.lines.parseJson; var data = await parsedStream.toList(); expect( data, @@ -154,7 +175,7 @@ void main() { } writer(); - var parsedStream = ndjson(ByteStream(pipe.read)); + var parsedStream = ByteStream(pipe.read).lines.parseJson; var data = await parsedStream.toList(); expect( data, @@ -173,7 +194,7 @@ void main() { } writer(); - var parsedStream = ndjson(ByteStream(pipe.read)); + var parsedStream = ByteStream(pipe.read).lines.parseJson; List result = []; Object? error; @@ -202,7 +223,7 @@ void main() { } writer(); - var parsedStream = ndjson(ByteStream(pipe.read)); + var parsedStream = ByteStream(pipe.read).lines.parseJson; Stream stream2 = genStream('S2:', Duration(milliseconds: 50)).asBroadcastStream(); diff --git a/packages/powersync/test/bucket_storage_test.dart b/packages/powersync_core/test/sync/bucket_storage_test.dart similarity index 81% rename from packages/powersync/test/bucket_storage_test.dart rename to packages/powersync_core/test/sync/bucket_storage_test.dart index 8b3cac41..496e5a49 100644 --- a/packages/powersync/test/bucket_storage_test.dart +++ b/packages/powersync_core/test/sync/bucket_storage_test.dart @@ -1,11 +1,12 @@ -import 'package:powersync/powersync.dart'; -import 'package:powersync/src/bucket_storage.dart'; -import 'package:powersync/src/sync_types.dart'; +import 'package:powersync_core/powersync_core.dart'; +import 'package:powersync_core/src/sync/bucket_storage.dart'; +import 'package:powersync_core/src/sync/protocol.dart'; import 'package:sqlite_async/sqlite3_common.dart'; import 'package:test/test.dart'; -import 'utils/abstract_test_utils.dart'; -import 'utils/test_utils_impl.dart'; +import '../utils/abstract_test_utils.dart'; +import '../utils/test_utils_impl.dart'; +import 'utils.dart'; final testUtils = TestUtils(); @@ -39,6 +40,10 @@ const removeAsset1_4 = OplogEntry( const removeAsset1_5 = OplogEntry( opId: '5', op: OpType.remove, rowType: 'assets', rowId: 'O1', checksum: 5); +SyncDataBatch syncDataBatch(List data) { + return SyncDataBatch(data); +} + void main() { group('Bucket Storage Tests', () { late PowerSyncDatabase powersync; @@ -88,7 +93,7 @@ void main() { expect(await bucketStorage.getBucketStates(), equals([])); expect(await bucketStorage.hasCompletedSync(), equals(false)); - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData( bucket: 'bucket1', data: [putAsset1_1, putAsset2_2, putAsset1_3], @@ -101,7 +106,7 @@ void main() { await syncLocalChecked(Checkpoint( lastOpId: '3', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: 6)])); + checksums: [checksum(bucket: 'bucket1', checksum: 6)])); await expectAsset1_3(); @@ -119,7 +124,7 @@ void main() { }); test('should get an object from multiple buckets', () async { - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData( bucket: 'bucket1', data: [putAsset1_3], @@ -128,8 +133,8 @@ void main() { ])); await syncLocalChecked(Checkpoint(lastOpId: '3', checksums: [ - BucketChecksum(bucket: 'bucket1', checksum: 3), - BucketChecksum(bucket: 'bucket2', checksum: 3) + checksum(bucket: 'bucket1', checksum: 3), + checksum(bucket: 'bucket2', checksum: 3) ])); await expectAsset1_3(); @@ -140,14 +145,14 @@ void main() { // In this case, there are two different versions in the different buckets. // While we should not get this with our server implementation, the client still specifies this behaviour: // The largest op_id wins. - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData(bucket: 'bucket1', data: [putAsset1_3]), SyncBucketData(bucket: 'bucket2', data: [putAsset1_1]) ])); await syncLocalChecked(Checkpoint(lastOpId: '3', checksums: [ - BucketChecksum(bucket: 'bucket1', checksum: 3), - BucketChecksum(bucket: 'bucket2', checksum: 1) + checksum(bucket: 'bucket1', checksum: 3), + checksum(bucket: 'bucket2', checksum: 1) ])); await expectAsset1_3(); @@ -155,14 +160,14 @@ void main() { test('should ignore a remove from one bucket', () async { // When we have 1 PUT and 1 REMOVE, the object must be kept. - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData(bucket: 'bucket1', data: [putAsset1_3]), SyncBucketData(bucket: 'bucket2', data: [putAsset1_3, removeAsset1_4]) ])); await syncLocalChecked(Checkpoint(lastOpId: '4', checksums: [ - BucketChecksum(bucket: 'bucket1', checksum: 3), - BucketChecksum(bucket: 'bucket2', checksum: 7) + checksum(bucket: 'bucket1', checksum: 3), + checksum(bucket: 'bucket2', checksum: 7) ])); await expectAsset1_3(); @@ -170,70 +175,70 @@ void main() { test('should remove when removed from all buckets', () async { // When we only have REMOVE left for an object, it must be deleted. - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData(bucket: 'bucket1', data: [putAsset1_3, removeAsset1_5]), SyncBucketData(bucket: 'bucket2', data: [putAsset1_3, removeAsset1_4]) ])); await syncLocalChecked(Checkpoint(lastOpId: '5', checksums: [ - BucketChecksum(bucket: 'bucket1', checksum: 8), - BucketChecksum(bucket: 'bucket2', checksum: 7) + checksum(bucket: 'bucket1', checksum: 8), + checksum(bucket: 'bucket2', checksum: 7) ])); await expectNoAssets(); }); test('put then remove', () async { - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData(bucket: 'bucket1', data: [putAsset1_3]), ])); await syncLocalChecked(Checkpoint(lastOpId: '3', checksums: [ - BucketChecksum(bucket: 'bucket1', checksum: 3), + checksum(bucket: 'bucket1', checksum: 3), ])); await expectAsset1_3(); - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData(bucket: 'bucket1', data: [removeAsset1_5]) ])); await syncLocalChecked(Checkpoint(lastOpId: '5', checksums: [ - BucketChecksum(bucket: 'bucket1', checksum: 8), + checksum(bucket: 'bucket1', checksum: 8), ])); await expectNoAssets(); }); test('blank remove', () async { - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData(bucket: 'bucket1', data: [putAsset1_3, removeAsset1_4]), ])); await syncLocalChecked(Checkpoint(lastOpId: '4', checksums: [ - BucketChecksum(bucket: 'bucket1', checksum: 7), + checksum(bucket: 'bucket1', checksum: 7), ])); await expectNoAssets(); - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData(bucket: 'bucket1', data: [removeAsset1_5]) ])); await syncLocalChecked(Checkpoint(lastOpId: '5', checksums: [ - BucketChecksum(bucket: 'bucket1', checksum: 12), + checksum(bucket: 'bucket1', checksum: 12), ])); await expectNoAssets(); }); test('put | put remove', () async { - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData(bucket: 'bucket1', data: [putAsset1_1]), ])); await syncLocalChecked(Checkpoint(lastOpId: '1', checksums: [ - BucketChecksum(bucket: 'bucket1', checksum: 1), + checksum(bucket: 'bucket1', checksum: 1), ])); expect( @@ -243,13 +248,13 @@ void main() { {'id': 'O1', 'description': 'bar', 'make': null} ])); - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData(bucket: 'bucket1', data: [putAsset1_3]), SyncBucketData(bucket: 'bucket1', data: [removeAsset1_5]) ])); await syncLocalChecked(Checkpoint(lastOpId: '5', checksums: [ - BucketChecksum(bucket: 'bucket1', checksum: 9), + checksum(bucket: 'bucket1', checksum: 9), ])); await expectNoAssets(); @@ -275,13 +280,13 @@ void main() { rowId: 'O1', checksum: 5); - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData( bucket: 'bucket1', data: [putAsset1_1, putAsset1_3, put4]), ])); await syncLocalChecked(Checkpoint(lastOpId: '4', checksums: [ - BucketChecksum(bucket: 'bucket1', checksum: 8), + checksum(bucket: 'bucket1', checksum: 8), ])); expect( @@ -291,12 +296,12 @@ void main() { {'id': 'O1', 'description': 'B', 'make': null} ])); - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData(bucket: 'bucket1', data: [remove5]), ])); await syncLocalChecked(Checkpoint(lastOpId: '5', checksums: [ - BucketChecksum(bucket: 'bucket1', checksum: 13), + checksum(bucket: 'bucket1', checksum: 13), ])); await expectAsset1_3(); @@ -304,15 +309,15 @@ void main() { test('should fail checksum validation', () async { // Simple checksum validation - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData( bucket: 'bucket1', data: [putAsset1_1, putAsset2_2, putAsset1_3]), ])); var result = await bucketStorage .syncLocalDatabase(Checkpoint(lastOpId: '3', checksums: [ - BucketChecksum(bucket: 'bucket1', checksum: 10), - BucketChecksum(bucket: 'bucket2', checksum: 1) + checksum(bucket: 'bucket1', checksum: 10), + checksum(bucket: 'bucket2', checksum: 1) ])); expect( result, @@ -325,7 +330,7 @@ void main() { }); test('should delete buckets', () async { - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData( bucket: 'bucket1', data: [putAsset1_3], @@ -340,7 +345,7 @@ void main() { // The delete only takes effect after syncLocal. await syncLocalChecked(Checkpoint(lastOpId: '3', checksums: [ - BucketChecksum(bucket: 'bucket1', checksum: 3), + checksum(bucket: 'bucket1', checksum: 3), ])); // Bucket is deleted, but object is still present in other buckets. @@ -354,7 +359,7 @@ void main() { test('should delete and re-create buckets', () async { // Save some data - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData( bucket: 'bucket1', data: [putAsset1_1], @@ -365,7 +370,7 @@ void main() { await bucketStorage.removeBuckets(['bucket1']); // Save some data again - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData( bucket: 'bucket1', data: [putAsset1_1, putAsset1_3], @@ -375,7 +380,7 @@ void main() { await bucketStorage.removeBuckets(['bucket1']); // Final save of data - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData( bucket: 'bucket1', data: [putAsset1_1, putAsset1_3], @@ -384,7 +389,7 @@ void main() { // Check that the data is there await syncLocalChecked(Checkpoint(lastOpId: '3', checksums: [ - BucketChecksum(bucket: 'bucket1', checksum: 4), + checksum(bucket: 'bucket1', checksum: 4), ])); await expectAsset1_3(); @@ -395,14 +400,14 @@ void main() { }); test('should handle MOVE', () async { - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData( bucket: 'bucket1', data: [OplogEntry(opId: '1', op: OpType.move, checksum: 1)], ), ])); - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData( bucket: 'bucket1', data: [putAsset1_3], @@ -411,14 +416,14 @@ void main() { await syncLocalChecked(Checkpoint( lastOpId: '3', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: 4)])); + checksums: [checksum(bucket: 'bucket1', checksum: 4)])); await expectAsset1_3(); }); test('should handle CLEAR', () async { // Save some data - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData( bucket: 'bucket1', data: [putAsset1_1], @@ -427,10 +432,10 @@ void main() { await syncLocalChecked(Checkpoint( lastOpId: '1', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: 1)])); + checksums: [checksum(bucket: 'bucket1', checksum: 1)])); // CLEAR, then save new data - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData( bucket: 'bucket1', data: [ @@ -449,7 +454,7 @@ void main() { await syncLocalChecked(Checkpoint( lastOpId: '3', // 2 + 3. 1 is replaced with 2. - checksums: [BucketChecksum(bucket: 'bucket1', checksum: 5)])); + checksums: [checksum(bucket: 'bucket1', checksum: 5)])); await expectNoAsset1(); expect( @@ -472,7 +477,7 @@ void main() { await powersync.initialize(); bucketStorage = BucketStorage(powersync); - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData( bucket: 'bucket1', data: [putAsset1_1, putAsset2_2, putAsset1_3], @@ -481,12 +486,12 @@ void main() { await syncLocalChecked(Checkpoint( lastOpId: '4', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: 6)])); + checksums: [checksum(bucket: 'bucket1', checksum: 6)])); await expectLater(() async { await powersync.execute('SELECT * FROM assets'); }, - throwsA((e) => + throwsA((dynamic e) => e is SqliteException && e.message.contains('no such table'))); await powersync.close(); @@ -499,7 +504,7 @@ void main() { }); test('should remove types', () async { - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData( bucket: 'bucket1', data: [putAsset1_1, putAsset2_2, putAsset1_3], @@ -508,7 +513,7 @@ void main() { await syncLocalChecked(Checkpoint( lastOpId: '3', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: 6)])); + checksums: [checksum(bucket: 'bucket1', checksum: 6)])); await expectAsset1_3(); @@ -521,7 +526,7 @@ void main() { await expectLater( () async => await powersync.execute('SELECT * FROM assets'), - throwsA((e) => + throwsA((dynamic e) => e is SqliteException && e.message.contains('no such table'))); await powersync.close(); @@ -537,7 +542,7 @@ void main() { // Test compacting behaviour. // This test relies heavily on internals, and will have to be updated when the compact implementation is updated. - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData( bucket: 'bucket1', data: [putAsset1_1, putAsset2_2, removeAsset1_4]) ])); @@ -545,14 +550,12 @@ void main() { await syncLocalChecked(Checkpoint( lastOpId: '4', writeCheckpoint: '4', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: 7)])); - - await bucketStorage.forceCompact(); + checksums: [checksum(bucket: 'bucket1', checksum: 7)])); await syncLocalChecked(Checkpoint( lastOpId: '4', writeCheckpoint: '4', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: 7)])); + checksums: [checksum(bucket: 'bucket1', checksum: 7)])); final stats = await powersync.execute( 'SELECT row_type as type, row_id as id, count(*) as count FROM ps_oplog GROUP BY row_type, row_id ORDER BY row_type, row_id'); @@ -564,7 +567,7 @@ void main() { }); test('should compact with checksum wrapping', () async { - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData(bucket: 'bucket1', data: [ OplogEntry( opId: '1', @@ -593,18 +596,12 @@ void main() { await syncLocalChecked(Checkpoint( lastOpId: '4', writeCheckpoint: '4', - checksums: [ - BucketChecksum(bucket: 'bucket1', checksum: 2147483642) - ])); - - await bucketStorage.forceCompact(); + checksums: [checksum(bucket: 'bucket1', checksum: 2147483642)])); await syncLocalChecked(Checkpoint( lastOpId: '4', writeCheckpoint: '4', - checksums: [ - BucketChecksum(bucket: 'bucket1', checksum: 2147483642) - ])); + checksums: [checksum(bucket: 'bucket1', checksum: 2147483642)])); final stats = await powersync.execute( 'SELECT row_type as type, row_id as id, count(*) as count FROM ps_oplog GROUP BY row_type, row_id ORDER BY row_type, row_id'); @@ -616,7 +613,7 @@ void main() { }); test('should compact with checksum wrapping (2)', () async { - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData(bucket: 'bucket1', data: [ OplogEntry( opId: '1', @@ -638,14 +635,12 @@ void main() { await syncLocalChecked(Checkpoint( lastOpId: '4', writeCheckpoint: '4', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: -3)])); - - await bucketStorage.forceCompact(); + checksums: [checksum(bucket: 'bucket1', checksum: -3)])); await syncLocalChecked(Checkpoint( lastOpId: '4', writeCheckpoint: '4', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: -3)])); + checksums: [checksum(bucket: 'bucket1', checksum: -3)])); final stats = await powersync.execute( 'SELECT row_type as type, row_id as id, count(*) as count FROM ps_oplog GROUP BY row_type, row_id ORDER BY row_type, row_id'); @@ -658,7 +653,7 @@ void main() { test('should not sync local db with pending crud - server removed', () async { - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData( bucket: 'bucket1', data: [putAsset1_1, putAsset2_2, putAsset1_3], @@ -667,7 +662,7 @@ void main() { await syncLocalChecked(Checkpoint( lastOpId: '3', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: 6)])); + checksums: [checksum(bucket: 'bucket1', checksum: 6)])); // Local save powersync.execute('INSERT INTO assets(id) VALUES(?)', ['O3']); @@ -681,7 +676,7 @@ void main() { final result = await bucketStorage.syncLocalDatabase(Checkpoint( lastOpId: '3', writeCheckpoint: '3', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: 6)])); + checksums: [checksum(bucket: 'bucket1', checksum: 6)])); expect(result, equals(SyncLocalDatabaseResult(ready: false))); final batch = await bucketStorage.getCrudBatch(); @@ -694,7 +689,7 @@ void main() { final result3 = await bucketStorage.syncLocalDatabase(Checkpoint( lastOpId: '3', writeCheckpoint: '3', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: 6)])); + checksums: [checksum(bucket: 'bucket1', checksum: 6)])); expect(result3, equals(SyncLocalDatabaseResult(ready: false))); // The data must still be present locally. @@ -705,14 +700,14 @@ void main() { ])); await bucketStorage.saveSyncData( - SyncDataBatch([SyncBucketData(bucket: 'bucket1', data: [])])); + syncDataBatch([SyncBucketData(bucket: 'bucket1', data: [])])); // Now we have synced the data back (or lack of data in this case), // so we can do a local sync. await syncLocalChecked(Checkpoint( lastOpId: '5', writeCheckpoint: '5', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: 6)])); + checksums: [checksum(bucket: 'bucket1', checksum: 6)])); // Since the object was not in the sync response, it is deleted. expect(await powersync.execute('SELECT id FROM assets WHERE id = \'O3\''), @@ -722,7 +717,7 @@ void main() { test( 'should not sync local db with pending crud when more crud is added (1)', () async { - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData( bucket: 'bucket1', data: [putAsset1_1, putAsset2_2, putAsset1_3], @@ -732,7 +727,7 @@ void main() { await syncLocalChecked(Checkpoint( lastOpId: '3', writeCheckpoint: '3', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: 6)])); + checksums: [checksum(bucket: 'bucket1', checksum: 6)])); // Local save powersync.execute('INSERT INTO assets(id) VALUES(?)', ['O3']); @@ -746,11 +741,11 @@ void main() { final result3 = await bucketStorage.syncLocalDatabase(Checkpoint( lastOpId: '3', writeCheckpoint: '3', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: 6)])); + checksums: [checksum(bucket: 'bucket1', checksum: 6)])); expect(result3, equals(SyncLocalDatabaseResult(ready: false))); await bucketStorage.saveSyncData( - SyncDataBatch([SyncBucketData(bucket: 'bucket1', data: [])])); + syncDataBatch([SyncBucketData(bucket: 'bucket1', data: [])])); // Add more data before syncLocalDatabase. powersync.execute('INSERT INTO assets(id) VALUES(?)', ['O4']); @@ -758,14 +753,14 @@ void main() { final result4 = await bucketStorage.syncLocalDatabase(Checkpoint( lastOpId: '5', writeCheckpoint: '5', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: 6)])); + checksums: [checksum(bucket: 'bucket1', checksum: 6)])); expect(result4, equals(SyncLocalDatabaseResult(ready: false))); }); test( 'should not sync local db with pending crud when more crud is added (2)', () async { - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData( bucket: 'bucket1', data: [putAsset1_1, putAsset2_2, putAsset1_3], @@ -775,7 +770,7 @@ void main() { await syncLocalChecked(Checkpoint( lastOpId: '3', writeCheckpoint: '3', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: 6)])); + checksums: [checksum(bucket: 'bucket1', checksum: 6)])); // Local save await powersync.execute('INSERT INTO assets(id) VALUES(?)', ['O3']); @@ -788,7 +783,7 @@ void main() { return '4'; }); - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData( bucket: 'bucket1', data: [], @@ -798,13 +793,13 @@ void main() { final result4 = await bucketStorage.syncLocalDatabase(Checkpoint( lastOpId: '5', writeCheckpoint: '5', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: 6)])); + checksums: [checksum(bucket: 'bucket1', checksum: 6)])); expect(result4, equals(SyncLocalDatabaseResult(ready: false))); }); test('should not sync local db with pending crud - update on server', () async { - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData( bucket: 'bucket1', data: [putAsset1_1, putAsset2_2, putAsset1_3], @@ -814,7 +809,7 @@ void main() { await syncLocalChecked(Checkpoint( lastOpId: '3', writeCheckpoint: '3', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: 6)])); + checksums: [checksum(bucket: 'bucket1', checksum: 6)])); // Local save powersync.execute('INSERT INTO assets(id) VALUES(?)', ['O3']); @@ -824,7 +819,7 @@ void main() { return '4'; }); - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData( bucket: 'bucket1', data: [ @@ -842,7 +837,7 @@ void main() { await syncLocalChecked(Checkpoint( lastOpId: '5', writeCheckpoint: '5', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: 11)])); + checksums: [checksum(bucket: 'bucket1', checksum: 11)])); expect( await powersync @@ -853,7 +848,7 @@ void main() { }); test('should revert a failing insert', () async { - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData( bucket: 'bucket1', data: [putAsset1_1, putAsset2_2, putAsset1_3], @@ -863,7 +858,7 @@ void main() { await syncLocalChecked(Checkpoint( lastOpId: '3', writeCheckpoint: '3', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: 6)])); + checksums: [checksum(bucket: 'bucket1', checksum: 6)])); // Local insert, later rejected by server await powersync.execute( @@ -885,7 +880,7 @@ void main() { await syncLocalChecked(Checkpoint( lastOpId: '3', writeCheckpoint: '4', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: 6)])); + checksums: [checksum(bucket: 'bucket1', checksum: 6)])); expect( await powersync @@ -894,7 +889,7 @@ void main() { }); test('should revert a failing delete', () async { - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData( bucket: 'bucket1', data: [putAsset1_1, putAsset2_2, putAsset1_3], @@ -904,7 +899,7 @@ void main() { await syncLocalChecked(Checkpoint( lastOpId: '3', writeCheckpoint: '3', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: 6)])); + checksums: [checksum(bucket: 'bucket1', checksum: 6)])); // Local delete, later rejected by server await powersync.execute('DELETE FROM assets WHERE id = ?', ['O2']); @@ -924,7 +919,7 @@ void main() { await syncLocalChecked(Checkpoint( lastOpId: '3', writeCheckpoint: '4', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: 6)])); + checksums: [checksum(bucket: 'bucket1', checksum: 6)])); expect( await powersync @@ -935,7 +930,7 @@ void main() { }); test('should revert a failing update', () async { - await bucketStorage.saveSyncData(SyncDataBatch([ + await bucketStorage.saveSyncData(syncDataBatch([ SyncBucketData( bucket: 'bucket1', data: [putAsset1_1, putAsset2_2, putAsset1_3], @@ -945,7 +940,7 @@ void main() { await syncLocalChecked(Checkpoint( lastOpId: '3', writeCheckpoint: '3', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: 6)])); + checksums: [checksum(bucket: 'bucket1', checksum: 6)])); // Local update, later rejected by server await powersync.execute( @@ -968,7 +963,7 @@ void main() { await syncLocalChecked(Checkpoint( lastOpId: '3', writeCheckpoint: '4', - checksums: [BucketChecksum(bucket: 'bucket1', checksum: 6)])); + checksums: [checksum(bucket: 'bucket1', checksum: 6)])); expect( await powersync diff --git a/packages/powersync_core/test/sync/in_memory_sync_test.dart b/packages/powersync_core/test/sync/in_memory_sync_test.dart new file mode 100644 index 00000000..87c18d82 --- /dev/null +++ b/packages/powersync_core/test/sync/in_memory_sync_test.dart @@ -0,0 +1,970 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:async/async.dart'; +import 'package:logging/logging.dart'; +import 'package:powersync_core/powersync_core.dart'; +import 'package:powersync_core/sqlite3_common.dart'; +import 'package:powersync_core/src/sync/protocol.dart'; +import 'package:shelf/shelf.dart'; +import 'package:shelf_router/shelf_router.dart'; +import 'package:test/test.dart'; + +import '../server/sync_server/in_memory_sync_server.dart'; +import '../utils/abstract_test_utils.dart'; +import '../utils/in_memory_http.dart'; +import '../utils/test_utils_impl.dart'; +import 'utils.dart'; + +void main() { + _declareTests( + 'dart sync client', + SyncOptions( + // ignore: deprecated_member_use_from_same_package + syncImplementation: SyncClientImplementation.dart, + retryDelay: Duration(milliseconds: 200), + ), + false, + ); + + group('rust sync client', () { + _declareTests( + 'json', + SyncOptions( + syncImplementation: SyncClientImplementation.rust, + retryDelay: Duration(milliseconds: 200)), + false, + ); + + _declareTests( + 'bson', + SyncOptions( + syncImplementation: SyncClientImplementation.rust, + retryDelay: Duration(milliseconds: 200)), + true, + ); + }); +} + +void _declareTests(String name, SyncOptions options, bool bson) { + final ignoredLogger = Logger.detached('powersync.test')..level = Level.OFF; + + group(name, () { + late final testUtils = TestUtils(); + + late TestPowerSyncFactory factory; + late CommonDatabase raw; + late TestDatabase database; + late MockSyncService syncService; + late Logger logger; + + var credentialsCallbackCount = 0; + Future Function(PowerSyncDatabase) uploadData = (db) async {}; + + Future connect() async { + final (client, server) = inMemoryServer(); + server.mount((req) => syncService.router(req)); + + database.httpClient = client; + await database.connect( + connector: TestConnector( + () async { + credentialsCallbackCount++; + return PowerSyncCredentials( + endpoint: server.url.toString(), + token: 'token$credentialsCallbackCount', + expiresAt: DateTime.now(), + ); + }, + uploadData: (db) => uploadData(db), + ), + options: options, + ); + } + + setUp(() async { + logger = Logger.detached('powersync.active')..level = Level.ALL; + credentialsCallbackCount = 0; + syncService = MockSyncService(useBson: bson); + + factory = await testUtils.testFactory(); + (raw, database) = await factory.openInMemoryDatabase(); + await database.initialize(); + }); + + tearDown(() async { + await database.close(); + await syncService.stop(); + }); + + Future> waitForConnection( + {bool expectNoWarnings = true}) async { + if (expectNoWarnings) { + logger.onRecord.listen((e) { + if (e.level >= Level.WARNING) { + fail('Unexpected log: $e, ${e.stackTrace}'); + } + }); + } + await connect(); + await syncService.waitForListener; + + expect(database.currentStatus.lastSyncedAt, isNull); + expect(database.currentStatus.downloading, isFalse); + final status = StreamQueue(database.statusStream); + addTearDown(status.cancel); + + syncService.addKeepAlive(); + await expectLater(status, + emitsThrough(isSyncStatus(connected: true, hasSynced: false))); + return status; + } + + test('persists completed sync information', () async { + final status = await waitForConnection(); + + syncService.addLine({ + 'checkpoint': { + 'last_op_id': '0', + 'buckets': [ + { + 'bucket': 'bkt', + 'checksum': 0, + } + ], + }, + }); + await expectLater(status, emits(isSyncStatus(downloading: true))); + + syncService.addLine({ + 'checkpoint_complete': {'last_op_id': '0'} + }); + await expectLater( + status, emits(isSyncStatus(downloading: false, hasSynced: true))); + await database.disconnect(); + + final independentDb = factory.wrapRaw(raw, logger: ignoredLogger); + addTearDown(independentDb.close); + // Even though this database doesn't have a sync client attached to it, + // is should reconstruct hasSynced from the database. + await independentDb.initialize(); + expect(independentDb.currentStatus.hasSynced, isTrue); + // A complete sync also means that all partial syncs have completed + expect( + independentDb.currentStatus + .statusForPriority(StreamPriority(3)) + .hasSynced, + isTrue); + }); + + // ignore: deprecated_member_use_from_same_package + if (options.syncImplementation == SyncClientImplementation.dart) { + test('can save independent buckets in same transaction', () async { + final status = await waitForConnection(); + + syncService.addLine({ + 'checkpoint': Checkpoint( + lastOpId: '0', + writeCheckpoint: null, + checksums: [ + BucketChecksum(bucket: 'a', checksum: 0, priority: 3), + BucketChecksum(bucket: 'b', checksum: 0, priority: 3), + ], + ) + }); + await expectLater(status, emits(isSyncStatus(downloading: true))); + + var commits = 0; + raw.commits.listen((_) => commits++); + + syncService + ..addLine({ + 'data': { + 'bucket': 'a', + 'data': >[ + { + 'op_id': '1', + 'op': 'PUT', + 'object_type': 'a', + 'object_id': '1', + 'checksum': 0, + 'data': '{}', + } + ], + } + }) + ..addLine({ + 'data': { + 'bucket': 'b', + 'data': >[ + { + 'op_id': '2', + 'op': 'PUT', + 'object_type': 'b', + 'object_id': '1', + 'checksum': 0, + 'data': '{}', + } + ], + } + }); + + // Wait for the operations to be inserted. + while (raw.select('SELECT * FROM ps_oplog;').length < 2) { + await pumpEventQueue(); + } + + // The two buckets should have been inserted in a single transaction + // because the messages were received in quick succession. + expect(commits, 1); + }); + } else { + // raw tables are only supported by the rust sync client + test('raw tables', () async { + final schema = Schema(const [], rawTables: [ + RawTable( + name: 'lists', + put: PendingStatement( + sql: 'INSERT OR REPLACE INTO lists (id, name) VALUES (?, ?)', + params: [ + PendingStatementValue.id(), + PendingStatementValue.column('name'), + ], + ), + delete: PendingStatement( + sql: 'DELETE FROM lists WHERE id = ?', + params: [ + PendingStatementValue.id(), + ], + ), + ), + ]); + + await database.execute( + 'CREATE TABLE lists (id TEXT NOT NULL PRIMARY KEY, name TEXT);'); + final query = StreamQueue( + database.watch('SELECT * FROM lists', throttle: Duration.zero)); + await expectLater(query, emits(isEmpty)); + + await database.updateSchema(schema); + await waitForConnection(); + + syncService + ..addLine({ + 'checkpoint': Checkpoint( + lastOpId: '1', + writeCheckpoint: null, + checksums: [ + BucketChecksum(bucket: 'a', priority: 3, checksum: 0) + ], + ) + }) + ..addLine({ + 'data': { + 'bucket': 'a', + 'data': [ + { + 'checksum': 0, + 'data': json.encode({'name': 'custom list'}), + 'op': 'PUT', + 'op_id': '1', + 'object_id': 'my_list', + 'object_type': 'lists' + } + ] + } + }) + ..addLine({ + 'checkpoint_complete': {'last_op_id': '1'} + }); + + await expectLater( + query, + emits([ + {'id': 'my_list', 'name': 'custom list'} + ]), + ); + + syncService + ..addLine({ + 'checkpoint': Checkpoint( + lastOpId: '2', + writeCheckpoint: null, + checksums: [ + BucketChecksum(bucket: 'a', priority: 3, checksum: 0) + ], + ) + }) + ..addLine({ + 'data': { + 'bucket': 'a', + 'data': [ + { + 'checksum': 0, + 'op': 'REMOVE', + 'op_id': '2', + 'object_id': 'my_list', + 'object_type': 'lists' + } + ] + } + }) + ..addLine({ + 'checkpoint_complete': {'last_op_id': '2'} + }); + + await expectLater(query, emits(isEmpty)); + }); + } + + group('partial sync', () { + test('updates sync state incrementally', () async { + final status = await waitForConnection(); + + final checksums = [ + for (var prio = 0; prio <= 3; prio++) + BucketChecksum( + bucket: 'prio$prio', priority: prio, checksum: 10 + prio) + ]; + syncService.addLine({ + 'checkpoint': Checkpoint( + lastOpId: '4', + writeCheckpoint: null, + checksums: checksums, + ) + }); + var operationId = 1; + + void addRow(int priority) { + syncService.addLine({ + 'data': { + 'bucket': 'prio$priority', + 'data': [ + { + 'checksum': priority + 10, + 'data': json.encode({'name': 'test', 'email': 'email'}), + 'op': 'PUT', + 'op_id': '${operationId++}', + 'object_id': 'prio$priority', + 'object_type': 'customers' + } + ] + } + }); + } + + // Receiving the checkpoint sets the state to downloading + await expectLater( + status, emits(isSyncStatus(downloading: true, hasSynced: false))); + + // Emit partial sync complete for each priority but the last. + for (var prio = 0; prio < 3; prio++) { + addRow(prio); + syncService.addLine({ + 'partial_checkpoint_complete': { + 'last_op_id': operationId.toString(), + 'priority': prio, + } + }); + + await expectLater( + status, + emitsThrough( + isSyncStatus(downloading: true, hasSynced: false).having( + (e) => e.statusForPriority(StreamPriority(0)).hasSynced, + 'status for $prio', + isTrue, + )), + ); + + await database.waitForFirstSync(priority: StreamPriority(prio)); + expect(await database.getAll('SELECT * FROM customers'), + hasLength(prio + 1)); + } + + // Complete the sync + addRow(3); + syncService.addLine({ + 'checkpoint_complete': {'last_op_id': operationId.toString()} + }); + + await expectLater(status, + emitsThrough(isSyncStatus(downloading: false, hasSynced: true))); + await database.waitForFirstSync(); + expect(await database.getAll('SELECT * FROM customers'), hasLength(4)); + }); + + test('remembers last partial sync state', () async { + final status = await waitForConnection(); + + syncService.addLine({ + 'checkpoint': Checkpoint( + lastOpId: '0', + writeCheckpoint: null, + checksums: [ + BucketChecksum(bucket: 'bkt', priority: 1, checksum: 0) + ], + ) + }); + await expectLater(status, emits(isSyncStatus(downloading: true))); + + syncService.addLine({ + 'partial_checkpoint_complete': { + 'last_op_id': '0', + 'priority': 1, + } + }); + await database.waitForFirstSync(priority: StreamPriority(1)); + expect(database.currentStatus.hasSynced, isFalse); + await database.disconnect(); + + final independentDb = factory.wrapRaw(raw, logger: ignoredLogger); + addTearDown(independentDb.close); + await independentDb.initialize(); + expect(independentDb.currentStatus.hasSynced, isFalse); + // Completing a sync for prio 1 implies a completed sync for prio 0 + expect( + independentDb.currentStatus + .statusForPriority(StreamPriority(0)) + .hasSynced, + isTrue); + expect( + independentDb.currentStatus + .statusForPriority(StreamPriority(3)) + .hasSynced, + isFalse); + }); + + test( + "multiple completed syncs don't create multiple sync state entries", + () async { + final status = await waitForConnection(); + + for (var i = 0; i < 5; i++) { + syncService.addLine({ + 'checkpoint': Checkpoint( + lastOpId: '0', + writeCheckpoint: null, + checksums: [ + BucketChecksum(bucket: 'bkt', priority: 1, checksum: 0) + ], + ) + }); + await expectLater(status, emits(isSyncStatus(downloading: true))); + + syncService.addLine({ + 'checkpoint_complete': { + 'last_op_id': '0', + } + }); + + await expectLater(status, emits(isSyncStatus(downloading: false))); + } + + final rows = await database.getAll('SELECT * FROM ps_sync_state;'); + expect(rows, hasLength(1)); + }, + ); + }); + + test('reconnects when token expires', () async { + await waitForConnection(); + expect(credentialsCallbackCount, 1); + // When the sync service says the token has expired + syncService + ..addLine({'token_expires_in': 0}) + ..endCurrentListener(); + + final nextRequest = await syncService.waitForListener; + expect(nextRequest.headers['Authorization'], 'Token token2'); + expect(credentialsCallbackCount, 2); + }); + + test('handles checkpoints during the upload process', () async { + final status = await waitForConnection(); + + Future expectCustomerRows(dynamic matcher) async { + final rows = await database.getAll('SELECT * FROM customers'); + expect(rows, matcher); + } + + final uploadStarted = Completer(); + final uploadFinished = Completer(); + + uploadData = (db) async { + if (await db.getCrudBatch() case final batch?) { + uploadStarted.complete(); + await uploadFinished.future; + batch.complete(); + } + }; + + // Trigger an upload + await database.execute( + 'INSERT INTO customers (id, name, email) VALUES (uuid(), ?, ?)', + ['local', 'local@example.org']); + await expectCustomerRows(hasLength(1)); + await uploadStarted.future; + + // Pretend that the connector takes forever in uploadData, but the data + // gets uploaded before the method returns. + syncService.addLine({ + 'checkpoint': Checkpoint( + writeCheckpoint: '1', + lastOpId: '2', + checksums: [BucketChecksum(bucket: 'a', priority: 3, checksum: 0)], + ) + }); + await expectLater(status, emitsThrough(isSyncStatus(downloading: true))); + + syncService + ..addLine({ + 'data': { + 'bucket': 'a', + 'data': [ + { + 'checksum': 0, + 'data': json.encode( + {'name': 'from local', 'email': 'local@example.org'}), + 'op': 'PUT', + 'op_id': '1', + 'object_id': '1', + 'object_type': 'customers' + }, + { + 'checksum': 0, + 'data': json.encode({'name': 'additional', 'email': ''}), + 'op': 'PUT', + 'op_id': '2', + 'object_id': '2', + 'object_type': 'customers' + } + ] + } + }) + ..addLine({ + 'checkpoint_complete': {'last_op_id': '2'} + }); + + // Despite receiving a valid checkpoint with two rows, it should not be + // visible because we have local data pending. + await expectCustomerRows(hasLength(1)); + + // Mark the upload as completed, this should trigger a write_checkpoint + // request. + final sentCheckpoint = Completer(); + syncService.writeCheckpoint = () { + sentCheckpoint.complete(); + return { + 'data': {'write_checkpoint': '1'} + }; + }; + uploadFinished.complete(); + await sentCheckpoint.future; + + // This should apply the checkpoint. + await expectLater(status, emitsThrough(isSyncStatus(downloading: false))); + + // Meaning that the two rows are now visible. + await expectCustomerRows(hasLength(2)); + }); + + group('reports progress', () { + var lastOpId = 0; + + setUp(() => lastOpId = 0); + + BucketChecksum bucket(String name, int count, {int priority = 3}) { + return BucketChecksum( + bucket: name, priority: priority, checksum: 0, count: count); + } + + void addDataLine(String bucket, int amount) { + syncService.addLine({ + 'data': { + 'bucket': bucket, + 'data': >[ + for (var i = 0; i < amount; i++) + { + 'op_id': '${++lastOpId}', + 'op': 'PUT', + 'object_type': bucket, + 'object_id': '$lastOpId', + 'checksum': 0, + 'data': '{}', + } + ], + } + }); + } + + void addCheckpointComplete([int? priority]) { + if (priority case final partial?) { + syncService.addLine({ + 'partial_checkpoint_complete': { + 'last_op_id': '$lastOpId', + 'priority': partial, + } + }); + } else { + syncService.addLine({ + 'checkpoint_complete': { + 'last_op_id': '$lastOpId', + } + }); + } + } + + Future expectProgress( + StreamQueue status, { + required Object total, + Map priorities = const {}, + }) async { + await expectLater( + status, + emitsThrough(isSyncStatus( + downloading: true, + downloadProgress: isSyncDownloadProgress( + progress: total, + priorities: priorities, + ), + )), + ); + } + + test('without priorities', () async { + final status = await waitForConnection(); + syncService.addLine({ + 'checkpoint': Checkpoint( + lastOpId: '10', + checksums: [bucket('a', 10)], + ) + }); + await expectProgress(status, total: progress(0, 10)); + + addDataLine('a', 10); + await expectProgress(status, total: progress(10, 10)); + + addCheckpointComplete(); + await expectLater(status, + emits(isSyncStatus(downloading: false, downloadProgress: isNull))); + + // Emit new data, progress should be 0/2 instead of 10/12 + syncService.addLine({ + 'checkpoint_diff': { + 'last_op_id': '12', + 'updated_buckets': [bucket('a', 12)], + 'removed_buckets': const [], + }, + }); + await expectProgress(status, total: progress(0, 2)); + addDataLine('a', 2); + await expectProgress(status, total: progress(2, 2)); + addCheckpointComplete(); + await expectLater(status, + emits(isSyncStatus(downloading: false, downloadProgress: isNull))); + }); + + test('interrupted sync', () async { + var status = await waitForConnection(); + syncService.addLine({ + 'checkpoint': Checkpoint( + lastOpId: '10', + checksums: [bucket('a', 10)], + ) + }); + await expectProgress(status, total: progress(0, 10)); + addDataLine('a', 5); + await expectProgress(status, total: progress(5, 10)); + + // Emulate the app closing - create a new independent sync client. + await database.disconnect(); + syncService.endCurrentListener(); + + status = await waitForConnection(); + + // Send same checkpoint again + syncService.addLine({ + 'checkpoint': Checkpoint( + lastOpId: '10', + checksums: [bucket('a', 10)], + ) + }); + + // Progress should be restored instead of saying e.g 0/5 now. + await expectProgress(status, total: progress(5, 10)); + addCheckpointComplete(); + await expectLater(status, + emits(isSyncStatus(downloading: false, downloadProgress: isNull))); + }); + + test('interrupted sync with new checkpoint', () async { + var status = await waitForConnection(); + syncService.addLine({ + 'checkpoint': Checkpoint( + lastOpId: '10', + checksums: [bucket('a', 10)], + ) + }); + await expectProgress(status, total: progress(0, 10)); + addDataLine('a', 5); + await expectProgress(status, total: progress(5, 10)); + + // Emulate the app closing - create a new independent sync client. + await database.disconnect(); + syncService.endCurrentListener(); + + status = await waitForConnection(); + + // Send checkpoint with additional data + syncService.addLine({ + 'checkpoint': Checkpoint( + lastOpId: '12', + checksums: [bucket('a', 12)], + ) + }); + + await expectProgress(status, total: progress(5, 12)); + addCheckpointComplete(); + await expectLater(status, + emits(isSyncStatus(downloading: false, downloadProgress: isNull))); + }); + + test('interrupt and defrag', () async { + var status = await waitForConnection(); + syncService.addLine({ + 'checkpoint': Checkpoint( + lastOpId: '10', + checksums: [bucket('a', 10)], + ) + }); + await expectProgress(status, total: progress(0, 10)); + addDataLine('a', 5); + await expectProgress(status, total: progress(5, 10)); + + // A sync rule deploy could reset buckets, making the new bucket smaller + // than the existing one. + await database.disconnect(); + syncService.endCurrentListener(); + + status = await waitForConnection(); + syncService.addLine({ + 'checkpoint': Checkpoint( + lastOpId: '14', + checksums: [bucket('a', 4)], + ) + }); + + // In this special case, don't report 5/4 as progress + await expectProgress(status, total: progress(0, 4)); + }); + + test('different priorities', () async { + var status = await waitForConnection(); + Future checkProgress(Object prio0, Object prio2) async { + await expectProgress( + status, + priorities: { + StreamPriority(0): prio0, + StreamPriority(2): prio2, + }, + total: prio2, + ); + } + + syncService.addLine({ + 'checkpoint': Checkpoint( + lastOpId: '10', + checksums: [ + bucket('a', 5, priority: 0), + bucket('b', 5, priority: 2) + ], + ), + }); + await checkProgress(progress(0, 5), progress(0, 10)); + + addDataLine('a', 5); + await checkProgress(progress(5, 5), progress(5, 10)); + + addCheckpointComplete(0); + await checkProgress(progress(5, 5), progress(5, 10)); + + addDataLine('b', 2); + await checkProgress(progress(5, 5), progress(7, 10)); + + // Before syncing b fully, send a new checkpoint + syncService.addLine({ + 'checkpoint': Checkpoint( + lastOpId: '14', + checksums: [ + bucket('a', 8, priority: 0), + bucket('b', 6, priority: 2) + ], + ), + }); + await checkProgress(progress(5, 8), progress(7, 14)); + + addDataLine('a', 3); + await checkProgress(progress(8, 8), progress(10, 14)); + + addCheckpointComplete(0); + + addDataLine('b', 4); + await checkProgress(progress(8, 8), progress(14, 14)); + + addCheckpointComplete(); + await expectLater(status, + emits(isSyncStatus(downloading: false, downloadProgress: isNull))); + }); + }); + + test('stopping closes connections', () async { + final status = await waitForConnection(); + + syncService.addLine({ + 'checkpoint': Checkpoint( + lastOpId: '4', + writeCheckpoint: null, + checksums: [checksum(bucket: 'a', checksum: 0)], + ) + }); + + await expectLater(status, emits(isSyncStatus(downloading: true))); + await database.disconnect(); + + expect(syncService.controller.hasListener, isFalse); + }); + + test('closes connection after failed checksum', () async { + final status = await waitForConnection(expectNoWarnings: false); + syncService.addLine({ + 'checkpoint': Checkpoint( + lastOpId: '4', + writeCheckpoint: null, + checksums: [checksum(bucket: 'a', checksum: 10)], + ) + }); + + await expectLater(status, emits(isSyncStatus(downloading: true))); + syncService.addLine({ + 'checkpoint_complete': {'last_op_id': '10'} + }); + syncService.endCurrentListener(); + + // Should reconnect after delay. + await Future.delayed(const Duration(milliseconds: 500)); + expect(syncService.controller.hasListener, isTrue); + }); + + test('closes connection after token expires', () async { + final status = await waitForConnection(expectNoWarnings: false); + syncService.addLine({ + 'checkpoint': Checkpoint( + lastOpId: '4', + writeCheckpoint: null, + checksums: [checksum(bucket: 'a', checksum: 10)], + ) + }); + + await expectLater(status, emits(isSyncStatus(downloading: true))); + syncService.addKeepAlive(0); + syncService.endCurrentListener(); + + // Should reconnect after delay. + await Future.delayed(const Duration(milliseconds: 500)); + expect(syncService.controller.hasListener, isTrue); + }); + + test('uploads writes made while offline', () async { + // Local write while not connected + await database.execute( + 'insert into customers (id, name) values (uuid(), ?)', + ['local customer']); + uploadData = (db) async { + final batch = await db.getNextCrudTransaction(); + if (batch != null) { + await batch.complete(); + } + }; + syncService.writeCheckpoint = () => { + 'data': {'write_checkpoint': '1'} + }; + + final query = StreamQueue(database + .watch('SELECT name FROM customers') + .map((e) => e.single['name'])); + expect(await query.next, 'local customer'); + + await waitForConnection(); + + syncService + ..addLine({ + 'checkpoint': Checkpoint( + lastOpId: '1', + writeCheckpoint: '1', + checksums: [BucketChecksum(bucket: 'a', priority: 3, checksum: 0)], + ) + }) + ..addLine({ + 'data': { + 'bucket': 'a', + 'data': >[ + { + 'op_id': '1', + 'op': 'PUT', + 'object_type': 'customers', + 'object_id': '1', + 'checksum': 0, + 'data': json.encode({'name': 'from server'}), + } + ], + } + }) + ..addLine({ + 'checkpoint_complete': {'last_op_id': '1'} + }); + + expect(await query.next, 'from server'); + }); + + group('abort', () { + test('during connect', () async { + final requestStarted = Completer(); + + syncService.router = Router() + ..post('/sync/stream', expectAsync1((Request request) async { + requestStarted.complete(); + + // emulate a network that never connects + await Completer().future; + })); + + await connect(); + await requestStarted.future; + expect(database.currentStatus, isSyncStatus(connecting: true)); + + await database.disconnect(); + expect(database.currentStatus.anyError, isNull); + }); + + test('during stream', () async { + final status = await waitForConnection(); + syncService.addLine({ + 'checkpoint': { + 'last_op_id': '0', + 'buckets': [ + { + 'bucket': 'bkt', + 'checksum': 0, + } + ], + }, + }); + await expectLater(status, emits(isSyncStatus(downloading: true))); + + await database.disconnect(); + expect(database.currentStatus.anyError, isNull); + }); + }); + }); +} diff --git a/packages/powersync_core/test/sync/options_test.dart b/packages/powersync_core/test/sync/options_test.dart new file mode 100644 index 00000000..d8784c9e --- /dev/null +++ b/packages/powersync_core/test/sync/options_test.dart @@ -0,0 +1,37 @@ +import 'package:powersync_core/src/sync/options.dart'; +import 'package:test/test.dart'; + +void main() { + group('sync options', () { + test('can merge with changes', () { + final a = ResolvedSyncOptions(SyncOptions( + params: {'client': 'a'}, + crudThrottleTime: const Duration(seconds: 1), + )); + + final (b, didChange) = a.applyFrom(SyncOptions( + params: {'client': 'a'}, + retryDelay: const Duration(seconds: 1), + )); + + expect(b.params, {'client': 'a'}); + expect(b.crudThrottleTime, const Duration(seconds: 1)); + expect(b.retryDelay, const Duration(seconds: 1)); + expect(didChange, isTrue); + }); + + test('can merge without changes', () { + final a = ResolvedSyncOptions(SyncOptions( + params: {'client': 'a'}, + crudThrottleTime: const Duration(seconds: 1), + )); + + final (_, didChange) = a.applyFrom(SyncOptions( + // This is the default, so no change from a + retryDelay: const Duration(seconds: 5), + )); + + expect(didChange, isFalse); + }); + }); +} diff --git a/packages/powersync_core/test/sync/stream_test.dart b/packages/powersync_core/test/sync/stream_test.dart new file mode 100644 index 00000000..1625656c --- /dev/null +++ b/packages/powersync_core/test/sync/stream_test.dart @@ -0,0 +1,263 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:async/async.dart'; +import 'package:logging/logging.dart'; +import 'package:powersync_core/powersync_core.dart'; + +import 'package:test/test.dart'; + +import '../server/sync_server/in_memory_sync_server.dart'; +import '../utils/abstract_test_utils.dart'; +import '../utils/in_memory_http.dart'; +import '../utils/test_utils_impl.dart'; +import 'utils.dart'; + +void main() { + late final testUtils = TestUtils(); + + late TestPowerSyncFactory factory; + + late TestDatabase database; + late MockSyncService syncService; + late Logger logger; + late SyncOptions options; + + var credentialsCallbackCount = 0; + + Future connect() async { + final (client, server) = inMemoryServer(); + server.mount(syncService.router.call); + + database.httpClient = client; + await database.connect( + connector: TestConnector( + () async { + credentialsCallbackCount++; + return PowerSyncCredentials( + endpoint: server.url.toString(), + token: 'token$credentialsCallbackCount', + expiresAt: DateTime.now(), + ); + }, + uploadData: (db) async {}, + ), + options: options, + ); + } + + setUp(() async { + options = SyncOptions(syncImplementation: SyncClientImplementation.rust); + logger = Logger.detached('powersync.active')..level = Level.ALL; + credentialsCallbackCount = 0; + syncService = MockSyncService(); + + factory = await testUtils.testFactory(); + (_, database) = await factory.openInMemoryDatabase(); + await database.initialize(); + }); + + tearDown(() async { + await database.close(); + await syncService.stop(); + }); + + Future> waitForConnection( + {bool expectNoWarnings = true}) async { + if (expectNoWarnings) { + logger.onRecord.listen((e) { + if (e.level >= Level.WARNING) { + fail('Unexpected log: $e, ${e.stackTrace}'); + } + }); + } + await connect(); + await syncService.waitForListener; + + expect(database.currentStatus.lastSyncedAt, isNull); + expect(database.currentStatus.downloading, isFalse); + final status = StreamQueue(database.statusStream); + addTearDown(status.cancel); + + syncService.addKeepAlive(); + await expectLater( + status, emitsThrough(isSyncStatus(connected: true, hasSynced: false))); + return status; + } + + test('can disable default streams', () async { + options = SyncOptions( + syncImplementation: SyncClientImplementation.rust, + includeDefaultStreams: false, + ); + + await waitForConnection(); + final request = await syncService.waitForListener; + expect(json.decode(await request.readAsString()), + containsPair('streams', containsPair('include_defaults', false))); + }); + + test('subscribes with streams', () async { + final a = await database.syncStream('stream', {'foo': 'a'}).subscribe(); + final b = await database.syncStream('stream', {'foo': 'b'}).subscribe( + priority: StreamPriority(1)); + + final statusStream = await waitForConnection(); + final request = await syncService.waitForListener; + expect( + json.decode(await request.readAsString()), + containsPair( + 'streams', + containsPair('subscriptions', [ + { + 'stream': 'stream', + 'parameters': {'foo': 'a'}, + 'override_priority': null, + }, + { + 'stream': 'stream', + 'parameters': {'foo': 'b'}, + 'override_priority': 1, + }, + ]), + ), + ); + + syncService.addLine( + checkpoint( + lastOpId: 0, + buckets: [ + bucketDescription('a', subscriptions: [ + {'sub': 0} + ]), + bucketDescription('b', priority: 1, subscriptions: [ + {'sub': 1} + ]) + ], + streams: [ + stream('stream', false), + ], + ), + ); + + var status = await statusStream.next; + for (final subscription in [a, b]) { + expect(status.forStream(subscription)!.subscription.active, true); + expect(status.forStream(subscription)!.subscription.lastSyncedAt, isNull); + expect( + status.forStream(subscription)!.subscription.hasExplicitSubscription, + true, + ); + } + + syncService.addLine(checkpointComplete(priority: 1)); + status = await statusStream.next; + expect(status.forStream(a)!.subscription.lastSyncedAt, isNull); + expect(status.forStream(b)!.subscription.lastSyncedAt, isNotNull); + await b.waitForFirstSync(); + + syncService.addLine(checkpointComplete()); + await a.waitForFirstSync(); + }); + + test('reports default streams', () async { + final status = await waitForConnection(); + syncService.addLine( + checkpoint(lastOpId: 0, streams: [stream('default_stream', true)]), + ); + + await expectLater( + status, + emits( + isSyncStatus( + syncStreams: [ + isStreamStatus( + subscription: isSyncSubscription( + name: 'default_stream', + parameters: null, + isDefault: true, + ), + ), + ], + ), + ), + ); + }); + + test('changes subscriptions dynamically', () async { + await waitForConnection(); + syncService.addKeepAlive(); + + final subscription = await database.syncStream('a').subscribe(); + syncService.endCurrentListener(); + final request = await syncService.waitForListener; + expect( + json.decode(await request.readAsString()), + containsPair( + 'streams', + containsPair('subscriptions', [ + { + 'stream': 'a', + 'parameters': null, + 'override_priority': null, + }, + ]), + ), + ); + + // Given that the subscription has a TTL, dropping the handle should not + // re-subscribe. + subscription.unsubscribe(); + await pumpEventQueue(); + expect(syncService.controller.hasListener, isTrue); + }); + + test('subscriptions update while offline', () async { + final stream = StreamQueue(database.statusStream); + + final subscription = await database.syncStream('foo').subscribe(); + var status = await stream.next; + expect(status.forStream(subscription), isNotNull); + }); + + test('unsubscribing multiple times has no effect', () async { + final a = await database.syncStream('a').subscribe(); + final aAgain = await database.syncStream('a').subscribe(); + a.unsubscribe(); + a.unsubscribe(); // Should not decrement the refcount again + + // Pretend the streams are expired - they should still be requested because + // the core extension extends the lifetime of streams currently referenced + // before connecting. + await database.execute( + 'UPDATE ps_stream_subscriptions SET expires_at = unixepoch() - 1000'); + + await waitForConnection(); + final request = await syncService.waitForListener; + expect( + json.decode(await request.readAsString()), + containsPair( + 'streams', + containsPair('subscriptions', isNotEmpty), + ), + ); + aAgain.unsubscribe(); + }); + + test('unsubscribeAll', () async { + final a = await database.syncStream('a').subscribe(); + await database.syncStream('a').unsubscribeAll(); + + // Despite a being active, it should not be requested. + await waitForConnection(); + final request = await syncService.waitForListener; + expect( + json.decode(await request.readAsString()), + containsPair( + 'streams', + containsPair('subscriptions', isEmpty), + ), + ); + a.unsubscribe(); + }); +} diff --git a/packages/powersync_core/test/sync/streaming_sync_test.dart b/packages/powersync_core/test/sync/streaming_sync_test.dart new file mode 100644 index 00000000..5017993f --- /dev/null +++ b/packages/powersync_core/test/sync/streaming_sync_test.dart @@ -0,0 +1,243 @@ +@TestOn('!browser') +library; + +// TODO setup hybrid server +import 'dart:async'; +import 'dart:math'; + +import 'package:logging/logging.dart'; +import 'package:powersync_core/powersync_core.dart'; +import 'package:test/test.dart'; + +import '../server/sync_server/in_memory_sync_server.dart'; +import '../test_server.dart'; +import '../utils/abstract_test_utils.dart'; +import '../utils/test_utils_impl.dart'; + +final testUtils = TestUtils(); + +void main() { + group('Streaming Sync Test', () { + late String path; + + setUp(() async { + path = testUtils.dbPath(); + await testUtils.cleanDb(path: path); + }); + + tearDown(() async { + await testUtils.cleanDb(path: path); + }); + + test('repeated connect and disconnect calls', () async { + final random = Random(); + final server = await createServer(); + final ignoreLogger = Logger.detached('powersync.test'); + + final pdb = + await testUtils.setupPowerSync(path: path, logger: ignoreLogger); + const options = SyncOptions(retryDelay: Duration(seconds: 5)); + final connector = TestConnector(() async { + return PowerSyncCredentials(endpoint: server.endpoint, token: 'token'); + }); + + Duration nextDelay() { + return Duration(milliseconds: random.nextInt(100)); + } + + Future connectAndDisconnect() async { + for (var i = 0; i < 10; i++) { + await Future.delayed(nextDelay()); + await pdb.connect(connector: connector, options: options); + + await Future.delayed(nextDelay()); + await pdb.disconnect(); + } + } + + // Create a bunch of tasks calling connect and disconnect() concurrently. + await Future.wait([for (var i = 0; i < 10; i++) connectAndDisconnect()]); + + expect(server.maxConnectionCount, lessThanOrEqualTo(1)); + server.close(); + }); + + test('can disconnect in fetchCredentials', () async { + final service = MockSyncService(); + final server = await createServer(mockSyncService: service); + final ignoreLogger = Logger.detached('powersync.test'); + + final pdb = + await testUtils.setupPowerSync(path: path, logger: ignoreLogger); + const options = SyncOptions(retryDelay: Duration(seconds: 5)); + final connector = TestConnector(expectAsync0(() async { + return PowerSyncCredentials(endpoint: server.endpoint, token: 'token'); + })); + + await pdb.connect(connector: connector, options: options); + while (server.connectionCount != 1) { + await Future.delayed(const Duration(milliseconds: 100)); + } + + service.addKeepAlive(60); + + final didDisconnect = Completer(); + + connector.fetchCredentialsCallback = expectAsync0(() async { + didDisconnect.complete(pdb.disconnect()); + + throw 'deliberate disconnect'; + }); + + service.addKeepAlive(0); + await didDisconnect.future; + expect(pdb.currentStatus.connected, isFalse); + // The error should be cleared after calling disconnect + expect(pdb.currentStatus.downloadError, isNull); + + // Wait for a short while to make sure the database doesn't reconnect. + for (var i = 0; i < 10; i++) { + expect(pdb.currentStatus.connecting, isFalse); + } + }); + + test('can connect as initial operation', () async { + final server = await createServer(); + final ignoreLogger = Logger.detached('powersync.test'); + + final pdb = await testUtils.setupPowerSync( + path: path, logger: ignoreLogger, initialize: false); + const options = SyncOptions(retryDelay: Duration(seconds: 5)); + + await pdb.connect( + connector: TestConnector(() async { + return PowerSyncCredentials( + endpoint: server.endpoint, token: 'token'); + }), + options: options, + ); + + await expectLater( + pdb.statusStream, + emitsThrough( + isA().having((e) => e.connected, 'connected', isTrue)), + ); + }); + + test('full powersync reconnect', () async { + // Test repeatedly creating new PowerSync connections, then disconnect + // and close the connection. + final random = Random(); + + for (var i = 0; i < 10; i++) { + var server = await createServer(); + + credentialsCallback() async { + return PowerSyncCredentials( + endpoint: server.endpoint, token: 'token'); + } + + final pdb = await testUtils.setupPowerSync(path: path); + const options = SyncOptions(retryDelay: Duration(seconds: 5)); + var connector = TestConnector(credentialsCallback); + pdb.connect(connector: connector, options: options); + + await Future.delayed(Duration(milliseconds: random.nextInt(100))); + if (random.nextBool()) { + server.close(); + } + + await pdb.close(); + + // Give some time for connections to close + final watch = Stopwatch()..start(); + while (server.connectionCount != 0 && watch.elapsedMilliseconds < 100) { + await Future.delayed( + Duration(milliseconds: random.nextInt(10))); + } + + expect(server.connectionCount, equals(0)); + expect(server.maxConnectionCount, lessThanOrEqualTo(1)); + + server.close(); + } + }); + + test('powersync connection errors', () async { + // Test repeatedly killing the streaming connection + // Errors like this are expected: + // + // [PowerSync] WARNING: 2023-06-29 16:05:24.810002: Sync error + // Connection closed while receiving data + // Write failed + // Connection refused + // + // Errors like this are not okay: + // [PowerSync] WARNING: 2023-06-29 16:10:17.667537: Sync Isolate error + // [Connection closed while receiving data, #0 IOClient.send. (package:http/src/io_client.dart:76:13) + + TestServer? server; + + credentialsCallback() async { + if (server == null) { + throw AssertionError('No active server'); + } + return PowerSyncCredentials(endpoint: server.endpoint, token: 'token'); + } + + final pdb = await testUtils.setupPowerSync(path: path); + const options = SyncOptions(retryDelay: Duration(seconds: 5)); + var connector = TestConnector(credentialsCallback); + pdb.connect(connector: connector, options: options); + + for (var i = 0; i < 10; i++) { + server = await createServer(); + + // var stream = impl.streamingSyncRequest(StreamingSyncRequest([])); + // 2ms: HttpException: HttpServer is not bound to a socket + // 20ms: Connection closed while receiving data + await Future.delayed(Duration(milliseconds: 20)); + server.close(); + } + await pdb.close(); + }); + + test('multiple connect calls', () async { + // Test calling connect() multiple times. + // We check that this does not cause multiple connections to be opened concurrently. + final random = Random(); + var server = await createServer(); + + credentialsCallback() async { + return PowerSyncCredentials(endpoint: server.endpoint, token: 'token'); + } + + final pdb = await testUtils.setupPowerSync(path: path); + const options = SyncOptions(retryDelay: Duration(seconds: 5)); + var connector = TestConnector(credentialsCallback); + pdb.connect(connector: connector, options: options); + pdb.connect(connector: connector, options: options); + + final watch = Stopwatch()..start(); + + // Wait for at least one connection + while (server.connectionCount < 1 && watch.elapsedMilliseconds < 500) { + await Future.delayed(Duration(milliseconds: random.nextInt(10))); + } + // Give some time for a second connection if any + await Future.delayed(Duration(milliseconds: random.nextInt(50))); + + await pdb.close(); + + // Give some time for connections to close + while (server.connectionCount != 0 && watch.elapsedMilliseconds < 1000) { + await Future.delayed(Duration(milliseconds: random.nextInt(10))); + } + + expect(server.connectionCount, equals(0)); + expect(server.maxConnectionCount, equals(1)); + + server.close(); + }); + }); +} diff --git a/packages/powersync_core/test/sync/sync_types_test.dart b/packages/powersync_core/test/sync/sync_types_test.dart new file mode 100644 index 00000000..5cd24c9d --- /dev/null +++ b/packages/powersync_core/test/sync/sync_types_test.dart @@ -0,0 +1,226 @@ +import 'dart:async'; + +import 'package:powersync_core/src/sync/sync_status.dart'; +import 'package:powersync_core/src/sync/protocol.dart'; +import 'package:test/test.dart'; + +void main() { + group('Sync types', () { + test('parses JSON stream', () { + final source = StreamController>(); + expect( + source.stream.transform(StreamingSyncLine.reader), + emitsInOrder([ + isA(), + isA(), + isA(), + isA(), + isA(), + isA(), + emitsDone, + ]), + ); + + source + ..add({'token_expires_in': 10}) + ..add({ + 'data': { + 'bucket': 'a', + 'data': >[], + 'hasMore': false + } + }) + ..add({ + 'checkpoint_complete': {'last_op_id': '10'} + }) + ..add({ + 'checkpoint': { + 'last_op_id': '10', + 'write_checkpoint': null, + 'buckets': >[], + } + }) + ..add({ + 'checkpoint_diff': { + 'last_op_id': '10', + 'write_checkpoint': null, + 'updated_buckets': >[], + 'removed_buckets': >[], + } + }) + ..add({'invalid_line': ''}) + ..close(); + }); + + test('can group data lines', () { + final source = StreamController>(); + expect( + source.stream.transform(StreamingSyncLine.reader), + emits( + isA() + .having((e) => e.buckets, 'buckets', hasLength(2)), + ), + ); + + source + ..add({ + 'data': { + 'bucket': 'a', + 'data': >[], + 'hasMore': false + } + }) + ..add({ + 'data': { + 'bucket': 'b', + 'data': >[], + 'hasMore': false + } + }); + }); + + test('flushes pending data lines before closing', () { + final source = StreamController>(); + expect( + source.stream.transform(StreamingSyncLine.reader), + emitsInOrder([ + isA(), + emitsDone, + ]), + ); + + source + ..add({ + 'data': { + 'bucket': 'a', + 'data': >[], + 'hasMore': false + } + }) + ..close(); + }); + + test('data line grouping keeps order', () { + final source = StreamController>(); + expect( + source.stream.transform(StreamingSyncLine.reader), + emitsInOrder([ + isA(), + isA(), + isA(), + emitsDone, + ]), + ); + + source + ..add({ + 'data': { + 'bucket': 'a', + 'data': >[], + 'hasMore': false + } + }) + ..add({ + 'checkpoint_complete': {'last_op_id': '10'} + }) + ..add({ + 'data': { + 'bucket': 'b', + 'data': >[], + 'hasMore': false + } + }) + ..close(); + }); + + test('does not combine large batches', () async { + final source = StreamController>(); + expect( + source.stream.transform(StreamingSyncLine.reader), + emitsInOrder([ + isA() + .having((e) => e.totalOperations, 'totalOperations', 1), + isA() + .having((e) => e.totalOperations, 'totalOperations', 150), + ]), + ); + + source + ..add({ + 'data': { + 'bucket': 'a', + 'data': >[ + { + 'op_id': '0', + 'op': 'PUT', + 'object_type': 'a', + 'object_id': '0', + 'checksum': 0, + 'data': {}, + } + ], + 'hasMore': false + } + }) + ..add({ + 'data': { + 'bucket': 'a', + 'data': >[ + for (var i = 1; i <= 150; i++) + { + 'op_id': '$i', + 'op': 'PUT', + 'object_type': 'a', + 'object_id': '$i', + 'checksum': 0, + 'data': {}, + } + ], + 'hasMore': false + } + }); + }); + + test('flushes when internal buffer gets too large', () { + final source = StreamController>(); + expect( + source.stream.transform(StreamingSyncLine.reader), + emitsInOrder([ + isA() + .having((e) => e.totalOperations, 'totalOperations', 1000), + isA() + .having((e) => e.totalOperations, 'totalOperations', 500), + ]), + ); + + // Add 1500 operations in chunks of 100 items. This should emit an + // 1000-item chunk and another one for the rest. + for (var i = 0; i < 15; i++) { + source.add({ + 'data': { + 'bucket': 'a', + 'data': >[ + for (var i = 0; i < 100; i++) + { + 'op_id': '1', + 'op': 'PUT', + 'object_type': 'a', + 'object_id': '1', + 'checksum': 0, + 'data': {}, + } + ], + 'hasMore': false + } + }); + } + }); + + test('stream priority comparisons', () { + expect(StreamPriority(0) < StreamPriority(3), isFalse); + expect(StreamPriority(0) > StreamPriority(3), isTrue); + expect(StreamPriority(0) >= StreamPriority(3), isTrue); + expect(StreamPriority(0) >= StreamPriority(0), isTrue); + }); + }); +} diff --git a/packages/powersync_core/test/sync/utils.dart b/packages/powersync_core/test/sync/utils.dart new file mode 100644 index 00000000..53654f12 --- /dev/null +++ b/packages/powersync_core/test/sync/utils.dart @@ -0,0 +1,136 @@ +import 'package:powersync_core/powersync_core.dart'; +import 'package:powersync_core/src/sync/protocol.dart'; +import 'package:test/test.dart'; + +TypeMatcher isSyncStatus({ + Object? downloading, + Object? connected, + Object? connecting, + Object? hasSynced, + Object? downloadProgress, + Object? syncStreams, +}) { + var matcher = isA(); + if (downloading != null) { + matcher = matcher.having((e) => e.downloading, 'downloading', downloading); + } + if (connected != null) { + matcher = matcher.having((e) => e.connected, 'connected', connected); + } + if (connecting != null) { + matcher = matcher.having((e) => e.connecting, 'connecting', connecting); + } + if (hasSynced != null) { + matcher = matcher.having((e) => e.hasSynced, 'hasSynced', hasSynced); + } + if (downloadProgress != null) { + matcher = matcher.having( + (e) => e.downloadProgress, 'downloadProgress', downloadProgress); + } + if (syncStreams != null) { + matcher = matcher.having((e) => e.syncStreams, 'syncStreams', syncStreams); + } + + return matcher; +} + +TypeMatcher isSyncDownloadProgress({ + required Object progress, + Map priorities = const {}, +}) { + var matcher = + isA().having((e) => e, 'untilCompletion', progress); + priorities.forEach((priority, expected) { + matcher = matcher.having( + (e) => e.untilPriority(priority), 'untilPriority($priority)', expected); + }); + + return matcher; +} + +TypeMatcher progress(int completed, int total) { + return isA() + .having((e) => e.downloadedOperations, 'completed', completed) + .having((e) => e.totalOperations, 'total', total); +} + +TypeMatcher isStreamStatus({ + required Object? subscription, + Object? progress, +}) { + var matcher = isA() + .having((e) => e.subscription, 'subscription', subscription); + if (progress case final progress?) { + matcher = matcher.having((e) => e.progress, 'progress', progress); + } + + return matcher; +} + +TypeMatcher isSyncSubscription({ + required Object name, + required Object? parameters, + bool? isDefault, +}) { + var matcher = isA() + .having((e) => e.name, 'name', name) + .having((e) => e.parameters, 'parameters', parameters); + + if (isDefault != null) { + matcher = matcher.having((e) => e.isDefault, 'isDefault', isDefault); + } + + return matcher; +} + +BucketChecksum checksum( + {required String bucket, required int checksum, int priority = 1}) { + return BucketChecksum(bucket: bucket, priority: priority, checksum: checksum); +} + +/// Creates a `checkpoint` line. +Object checkpoint({ + required int lastOpId, + List buckets = const [], + String? writeCheckpoint, + List streams = const [], +}) { + return { + 'checkpoint': { + 'last_op_id': '$lastOpId', + 'write_checkpoint': null, + 'buckets': buckets, + 'streams': streams, + } + }; +} + +Object stream(String name, bool isDefault, {List errors = const []}) { + return {'name': name, 'is_default': isDefault, 'errors': errors}; +} + +/// Creates a `checkpoint_complete` or `partial_checkpoint_complete` line. +Object checkpointComplete({int? priority, String lastOpId = '1'}) { + return { + priority == null ? 'checkpoint_complete' : 'partial_checkpoint_complete': { + 'last_op_id': lastOpId, + if (priority != null) 'priority': priority, + }, + }; +} + +Object bucketDescription( + String name, { + int checksum = 0, + int priority = 3, + int count = 1, + Object? subscriptions, +}) { + return { + 'bucket': name, + 'checksum': checksum, + 'priority': priority, + 'count': count, + if (subscriptions != null) 'subscriptions': subscriptions, + }; +} diff --git a/packages/powersync/test/test_server.dart b/packages/powersync_core/test/test_server.dart similarity index 74% rename from packages/powersync/test/test_server.dart rename to packages/powersync_core/test/test_server.dart index 8054582e..372da797 100644 --- a/packages/powersync/test/test_server.dart +++ b/packages/powersync_core/test/test_server.dart @@ -8,7 +8,9 @@ import 'package:shelf/shelf.dart'; import 'package:shelf/shelf_io.dart' as shelf_io; import 'package:shelf_router/shelf_router.dart'; -class TestServer { +import 'server/sync_server/in_memory_sync_server.dart'; + +final class TestServer { late HttpServer server; Router app = Router(); int maxConnectionCount = 0; @@ -16,10 +18,11 @@ class TestServer { TestServer({this.tokenExpiresIn = 65}); - Future init() async { + Future init({MockSyncService? mockSyncService}) async { app.post('/sync/stream', handleSyncStream); // Open on an arbitrary open port - server = await shelf_io.serve(app.call, 'localhost', 0); + server = await shelf_io.serve( + mockSyncService?.router.call ?? app.call, 'localhost', 0); } String get endpoint { @@ -34,6 +37,9 @@ class TestServer { return server.connectionsInfo(); } + /// The default response if no [MockSyncService] has been passed to [init]. + /// + /// This will emit keepalive messages frequently. Future handleSyncStream(Request request) async { maxConnectionCount = max(connectionCount, maxConnectionCount); @@ -41,7 +47,7 @@ class TestServer { var blob = "*" * 5000; for (var i = 0; i < 50; i++) { yield {"token_expires_in": tokenExpiresIn, "blob": blob}; - await Future.delayed(Duration(microseconds: 1)); + await Future.delayed(Duration(microseconds: 1)); } } @@ -61,9 +67,9 @@ class TestServer { } } -Future createServer() async { +Future createServer({MockSyncService? mockSyncService}) async { var server = TestServer(); - await server.init(); + await server.init(mockSyncService: mockSyncService); return server; } diff --git a/packages/powersync/test/upload_test.dart b/packages/powersync_core/test/upload_test.dart similarity index 77% rename from packages/powersync/test/upload_test.dart rename to packages/powersync_core/test/upload_test.dart index c3fa603e..d0c8f23a 100644 --- a/packages/powersync/test/upload_test.dart +++ b/packages/powersync_core/test/upload_test.dart @@ -1,6 +1,7 @@ @TestOn('!browser') +library; -import 'package:powersync/powersync.dart'; +import 'package:powersync_core/powersync_core.dart'; import 'package:test/test.dart'; import 'test_server.dart'; @@ -13,23 +14,6 @@ const testId2 = "2290de4f-0488-4e50-abed-f8e8eb1d0b43"; const partialWarning = 'Potentially previously uploaded CRUD entries are still present'; -class TestConnector extends PowerSyncBackendConnector { - final Function _fetchCredentials; - final Future Function(PowerSyncDatabase database) _uploadData; - - TestConnector(this._fetchCredentials, this._uploadData); - - @override - Future fetchCredentials() { - return _fetchCredentials(); - } - - @override - Future uploadData(PowerSyncDatabase database) async { - return _uploadData(database); - } -} - void main() { group('CRUD Tests', () { late PowerSyncDatabase powersync; @@ -68,8 +52,10 @@ void main() { await testUtils.setupPowerSync(path: path, logger: testWarningLogger); // Use a short retry delay here. // A zero retry delay makes this test unstable, since it expects `2` error logs later. + // ignore: deprecated_member_use_from_same_package powersync.retryDelay = Duration(milliseconds: 100); - var connector = TestConnector(credentialsCallback, uploadData); + var connector = + TestConnector(credentialsCallback, uploadData: uploadData); powersync.connect(connector: connector); // Create something with CRUD in it. @@ -77,7 +63,7 @@ void main() { 'INSERT INTO assets(id, description) VALUES(?, ?)', [testId, 'test']); // Wait for the uploadData to be called. - await Future.delayed(Duration(milliseconds: 100)); + await Future.delayed(Duration(milliseconds: 100)); // Create something else with CRUD in it. await powersync.execute( diff --git a/packages/powersync_core/test/utils/abstract_test_utils.dart b/packages/powersync_core/test/utils/abstract_test_utils.dart new file mode 100644 index 00000000..96469c5a --- /dev/null +++ b/packages/powersync_core/test/utils/abstract_test_utils.dart @@ -0,0 +1,256 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:logging/logging.dart'; +import 'package:powersync_core/powersync_core.dart'; +import 'package:powersync_core/src/abort_controller.dart'; +import 'package:powersync_core/src/database/powersync_db_mixin.dart'; +import 'package:powersync_core/src/sync/bucket_storage.dart'; +import 'package:powersync_core/src/sync/internal_connector.dart'; +import 'package:powersync_core/src/sync/options.dart'; +import 'package:powersync_core/src/sync/streaming_sync.dart'; +import 'package:sqlite_async/sqlite3_common.dart'; +import 'package:sqlite_async/sqlite_async.dart'; +import 'package:test/test.dart'; +import 'package:test_api/src/backend/invoker.dart'; + +const schema = Schema([ + Table('assets', [ + Column.text('created_at'), + Column.text('make'), + Column.text('model'), + Column.text('serial_number'), + Column.integer('quantity'), + Column.text('user_id'), + Column.text('customer_id'), + Column.text('description'), + ], indexes: [ + Index('makemodel', [IndexedColumn('make'), IndexedColumn('model')]) + ]), + Table('customers', [Column.text('name'), Column.text('email')]) +]); + +const defaultSchema = schema; + +final testLogger = _makeTestLogger(); + +final testWarningLogger = _makeTestLogger(level: Level.WARNING); + +Logger _makeTestLogger({Level level = Level.ALL, String? name}) { + final logger = Logger.detached(name ?? 'PowerSync Tests'); + logger.level = level; + logger.onRecord.listen((record) { + print( + '[${record.loggerName}] ${record.level.name}: ${record.time}: ${record.message}'); + if (record.error != null) { + print(record.error); + } + if (record.stackTrace != null) { + print(record.stackTrace); + } + + if (record.error != null && record.level >= Level.SEVERE) { + // Hack to fail the test if a SEVERE error is logged. + // Not ideal, but works to catch "Sync Isolate error". + uncaughtError() async { + throw 'Unexpected severe error on logger: ${record.error!}'; + } + + uncaughtError(); + } + }); + return logger; +} + +abstract mixin class TestPowerSyncFactory implements PowerSyncOpenFactory { + Future openRawInMemoryDatabase(); + + Future<(CommonDatabase, TestDatabase)> openInMemoryDatabase({ + Schema? schema, + Logger? logger, + }) async { + final raw = await openRawInMemoryDatabase(); + return (raw, wrapRaw(raw, customSchema: schema, logger: logger)); + } + + TestDatabase wrapRaw( + CommonDatabase raw, { + Logger? logger, + Schema? customSchema, + }) { + return TestDatabase( + database: SqliteDatabase.singleConnection( + SqliteConnection.synchronousWrapper(raw)), + logger: logger ?? Logger.detached('PowerSync.test'), + schema: customSchema ?? schema, + ); + } +} + +abstract class AbstractTestUtils { + String get _testName => Invoker.current!.liveTest.test.name; + + String dbPath() { + var testShortName = + _testName.replaceAll(RegExp(r'[\s\./]'), '_').toLowerCase(); + var dbName = "test-db/$testShortName.db"; + return dbName; + } + + /// Generates a test open factory + Future testFactory( + {String? path, + String sqlitePath = '', + SqliteOptions options = const SqliteOptions.defaults()}); + + /// Creates a SqliteDatabaseConnection + Future setupPowerSync({ + String? path, + Schema? schema, + Logger? logger, + bool initialize = true, + }) async { + final db = PowerSyncDatabase.withFactory(await testFactory(path: path), + schema: schema ?? defaultSchema, + logger: logger ?? _makeTestLogger(name: _testName)); + if (initialize) { + await db.initialize(); + } + addTearDown(db.close); + return db; + } + + Future setupSqlite( + {required PowerSyncDatabase powersync}) async { + await powersync.initialize(); + + final sqliteDb = + await powersync.isolateConnectionFactory().openRawDatabase(); + + return sqliteDb; + } + + /// Deletes any DB data + Future cleanDb({required String path}); +} + +class TestConnector extends PowerSyncBackendConnector { + Future Function() fetchCredentialsCallback; + Future Function(PowerSyncDatabase)? uploadDataCallback; + + TestConnector(this.fetchCredentialsCallback, + {Future Function(PowerSyncDatabase)? uploadData}) + : uploadDataCallback = uploadData; + + @override + Future fetchCredentials() { + return fetchCredentialsCallback(); + } + + @override + Future uploadData(PowerSyncDatabase database) async { + await uploadDataCallback?.call(database); + } +} + +/// A [PowerSyncDatabase] implemented by a single in-memory database connection +/// and a mock-HTTP sync client. +/// +/// This ensures tests for sync cover the `ConnectionManager` and other methods +/// exposed by the mixin. +final class TestDatabase + with SqliteQueries, PowerSyncDatabaseMixin + implements PowerSyncDatabase { + @override + final SqliteDatabase database; + @override + final Logger logger; + @override + Schema schema; + + @override + late final Future isInitialized; + + Client? httpClient; + + TestDatabase({ + required this.database, + required this.logger, + required this.schema, + }) { + isInitialized = baseInit(); + } + + @override + Future connectInternal({ + required PowerSyncBackendConnector connector, + required ResolvedSyncOptions options, + required List initiallyActiveStreams, + required Stream> activeStreams, + required AbortController abort, + required Zone asyncWorkZone, + }) async { + final impl = StreamingSyncImplementation( + adapter: BucketStorage(this), + schemaJson: jsonEncode(schema), + client: httpClient!, + options: options, + connector: InternalConnector.wrap(connector, this), + logger: logger, + crudUpdateTriggerStream: database + .onChange(['ps_crud'], throttle: const Duration(milliseconds: 10)), + activeSubscriptions: initiallyActiveStreams, + ); + impl.statusStream.listen(setStatus); + + asyncWorkZone.run(impl.streamingSync); + final subscriptions = activeStreams.listen(impl.updateSubscriptions); + + abort.onAbort.then((_) async { + subscriptions.cancel(); + await impl.abort(); + abort.completeAbort(); + }).ignore(); + } + + @override + Future readLock(Future Function(SqliteReadContext tx) callback, + {String? debugContext, Duration? lockTimeout}) async { + await isInitialized; + return database.readLock(callback, + debugContext: debugContext, lockTimeout: lockTimeout); + } + + @override + Future writeLock(Future Function(SqliteWriteContext tx) callback, + {String? debugContext, Duration? lockTimeout}) async { + await isInitialized; + return database.writeLock(callback, + debugContext: debugContext, lockTimeout: lockTimeout); + } +} + +extension MockSync on PowerSyncDatabase { + StreamingSyncImplementation connectWithMockService( + Client client, + PowerSyncBackendConnector connector, { + Logger? logger, + SyncOptions options = const SyncOptions(retryDelay: Duration(seconds: 5)), + Schema? customSchema, + }) { + final impl = StreamingSyncImplementation( + adapter: BucketStorage(this), + schemaJson: jsonEncode(customSchema ?? schema), + client: client, + options: ResolvedSyncOptions(options), + connector: InternalConnector.wrap(connector, this), + logger: logger, + crudUpdateTriggerStream: database + .onChange(['ps_crud'], throttle: const Duration(milliseconds: 10)), + ); + impl.statusStream.listen(setStatus); + + return impl; + } +} diff --git a/packages/powersync_core/test/utils/in_memory_http.dart b/packages/powersync_core/test/utils/in_memory_http.dart new file mode 100644 index 00000000..a35d6a09 --- /dev/null +++ b/packages/powersync_core/test/utils/in_memory_http.dart @@ -0,0 +1,101 @@ +import 'package:http/http.dart'; +import 'package:http/testing.dart'; +import 'package:shelf/shelf.dart' as shelf; + +final Uri mockHttpUri = Uri.parse('https://testing.powersync.com/'); + +/// Returns a [Client] that can send HTTP requests to the returned +/// [shelf.Server]. +/// +/// The server can be used to serve shelf routes via [shelf.Server.mount]. +(Client, shelf.Server) inMemoryServer() { + final server = _MockServer(); + final client = MockClient.streaming(server.handleRequest); + + return (client, server); +} + +final class _MockServer implements shelf.Server { + shelf.Handler? _handler; + + @override + void mount(shelf.Handler handler) { + if (_handler != null) { + throw StateError('already has a handler'); + } + + _handler = handler; + } + + @override + Future close() async {} + + @override + Uri get url => mockHttpUri; + + Future handleRequest( + BaseRequest request, ByteStream body) async { + final cancellationFuture = switch (request) { + Abortable(:final abortTrigger) => abortTrigger, + _ => null, + }; + + if (_handler case final endpoint?) { + final shelfRequest = shelf.Request( + request.method, + request.url, + headers: request.headers, + body: body, + ); + + final shelfResponse = await Future.any([ + Future.sync(() => endpoint(shelfRequest)), + if (cancellationFuture != null) + cancellationFuture.then((_) { + throw RequestAbortedException(); + }), + ]); + + return StreamedResponse( + shelfResponse.read().injectCancellation(cancellationFuture), + shelfResponse.statusCode, + headers: shelfResponse.headers, + ); + } else { + throw StateError('Request before handler was set on mock server'); + } + } +} + +extension on Stream { + Stream injectCancellation(Future? token) { + if (token == null) { + return this; + } + + return Stream.multi( + (listener) { + final subscription = listen( + listener.addSync, + onError: listener.addErrorSync, + onDone: listener.closeSync, + ); + + listener + ..onPause = subscription.pause + ..onResume = subscription.resume + ..onCancel = subscription.cancel; + + token.whenComplete(() { + if (!listener.isClosed) { + listener + ..addErrorSync(RequestAbortedException()) + ..closeSync(); + subscription.cancel(); + } + }); + }, + isBroadcast: isBroadcast, + ); + } +} diff --git a/packages/powersync/test/utils/native_test_utils.dart b/packages/powersync_core/test/utils/native_test_utils.dart similarity index 70% rename from packages/powersync/test/utils/native_test_utils.dart rename to packages/powersync_core/test/utils/native_test_utils.dart index 8be005e9..04bc0e73 100644 --- a/packages/powersync/test/utils/native_test_utils.dart +++ b/packages/powersync_core/test/utils/native_test_utils.dart @@ -1,7 +1,8 @@ import 'dart:async'; import 'dart:ffi'; import 'dart:io'; -import 'package:powersync/powersync.dart'; +import 'package:powersync_core/powersync_core.dart'; +import 'package:powersync_core/sqlite3.dart'; import 'package:sqlite_async/sqlite3_common.dart'; import 'package:sqlite_async/sqlite_async.dart'; import 'package:sqlite3/open.dart' as sqlite_open; @@ -10,20 +11,37 @@ import 'abstract_test_utils.dart'; const defaultSqlitePath = 'libsqlite3.so.0'; -class TestOpenFactory extends PowerSyncOpenFactory { +class TestOpenFactory extends PowerSyncOpenFactory with TestPowerSyncFactory { TestOpenFactory({required super.path}); - @override - CommonDatabase open(SqliteOpenOptions options) { + void applyOpenOverride() { sqlite_open.open.overrideFor(sqlite_open.OperatingSystem.linux, () { return DynamicLibrary.open('libsqlite3.so.0'); }); sqlite_open.open.overrideFor(sqlite_open.OperatingSystem.macOS, () { + // Prefer using Homebrew's SQLite which allows loading extensions. + const fromHomebrew = '/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib'; + if (File(fromHomebrew).existsSync()) { + return DynamicLibrary.open(fromHomebrew); + } + return DynamicLibrary.open('libsqlite3.dylib'); }); + } + + @override + CommonDatabase open(SqliteOpenOptions options) { + applyOpenOverride(); return super.open(options); } + @override + void enableExtension() { + var powersyncLib = getLibraryForPlatform(); + sqlite3.ensureExtensionLoaded(SqliteExtension.inLibrary( + DynamicLibrary.open(powersyncLib), 'sqlite3_powersync_init')); + } + @override String getLibraryForPlatform({String? path = "."}) { switch (Abi.current()) { @@ -52,6 +70,22 @@ class TestOpenFactory extends PowerSyncOpenFactory { ); } } + + @override + Future openRawInMemoryDatabase() async { + applyOpenOverride(); + + try { + enableExtension(); + } on PowersyncNotReadyException catch (e) { + autoLogger.severe(e.message); + rethrow; + } + + final db = sqlite3.openInMemory(); + setupFunctions(db); + return db; + } } class TestUtils extends AbstractTestUtils { diff --git a/packages/powersync_core/test/utils/stub_test_utils.dart b/packages/powersync_core/test/utils/stub_test_utils.dart new file mode 100644 index 00000000..3755e47f --- /dev/null +++ b/packages/powersync_core/test/utils/stub_test_utils.dart @@ -0,0 +1,18 @@ +import 'package:sqlite_async/src/sqlite_options.dart'; + +import 'abstract_test_utils.dart'; + +class TestUtils extends AbstractTestUtils { + @override + Future testFactory( + {String? path, + String sqlitePath = '', + SqliteOptions options = const SqliteOptions.defaults()}) { + throw UnimplementedError(); + } + + @override + Future cleanDb({required String path}) { + throw UnimplementedError(); + } +} diff --git a/packages/powersync/test/utils/test_utils_impl.dart b/packages/powersync_core/test/utils/test_utils_impl.dart similarity index 72% rename from packages/powersync/test/utils/test_utils_impl.dart rename to packages/powersync_core/test/utils/test_utils_impl.dart index 99a34d39..3406d1ec 100644 --- a/packages/powersync/test/utils/test_utils_impl.dart +++ b/packages/powersync_core/test/utils/test_utils_impl.dart @@ -2,4 +2,4 @@ export 'stub_test_utils.dart' // ignore: uri_does_not_exist if (dart.library.io) 'native_test_utils.dart' // ignore: uri_does_not_exist - if (dart.library.html) 'web_test_utils.dart'; + if (dart.library.js_interop) 'web_test_utils.dart'; diff --git a/packages/powersync/test/utils/web_test_utils.dart b/packages/powersync_core/test/utils/web_test_utils.dart similarity index 53% rename from packages/powersync/test/utils/web_test_utils.dart rename to packages/powersync_core/test/utils/web_test_utils.dart index a358d0ff..890cea38 100644 --- a/packages/powersync/test/utils/web_test_utils.dart +++ b/packages/powersync_core/test/utils/web_test_utils.dart @@ -1,17 +1,40 @@ import 'dart:async'; -import 'dart:html'; +import 'dart:js_interop'; -import 'package:js/js.dart'; import 'package:logging/logging.dart'; -import 'package:powersync/powersync.dart'; -import 'package:sqlite_async/sqlite3_common.dart'; +import 'package:powersync_core/powersync_core.dart'; +import 'package:sqlite_async/sqlite3_wasm.dart'; import 'package:sqlite_async/sqlite_async.dart'; import 'package:test/test.dart'; +import 'package:web/web.dart' show Blob, BlobPropertyBag; import 'abstract_test_utils.dart'; @JS('URL.createObjectURL') external String _createObjectURL(Blob blob); +class TestOpenFactory extends PowerSyncOpenFactory with TestPowerSyncFactory { + TestOpenFactory({required super.path, super.sqliteOptions}); + + @override + Future openRawInMemoryDatabase() async { + final sqlite = await WasmSqlite3.loadFromUrl( + Uri.parse(sqliteOptions.webSqliteOptions.wasmUri)); + sqlite.registerVirtualFileSystem(InMemoryFileSystem(), makeDefault: true); + + final db = sqlite.openInMemory(); + + try { + enableExtension(); + } on PowersyncNotReadyException catch (e) { + autoLogger.severe(e.message); + rethrow; + } + + setupFunctions(db); + return db; + } +} + class TestUtils extends AbstractTestUtils { late Future _isInitialized; late final String sqlite3WASMUri; @@ -29,8 +52,9 @@ class TestUtils extends AbstractTestUtils { // Cross origin workers are not supported, but we can supply a Blob final workerUriSource = 'http://localhost:$port/powersync_db.worker.js'; - final blob = Blob(['importScripts("$workerUriSource");'], - 'application/javascript'); + final blob = Blob( + ['importScripts("$workerUriSource");'.toJS].toJS, + BlobPropertyBag(type: 'application/javascript')); workerUri = _createObjectURL(blob); } @@ -38,23 +62,27 @@ class TestUtils extends AbstractTestUtils { Future cleanDb({required String path}) async {} @override - Future testFactory( + Future testFactory( {String? path, - String? sqlitePath, + String sqlitePath = '', SqliteOptions options = const SqliteOptions.defaults()}) async { await _isInitialized; final webOptions = SqliteOptions( webSqliteOptions: WebSqliteOptions(wasmUri: sqlite3WASMUri, workerUri: workerUri)); - return super.testFactory(path: path, options: webOptions); + return TestOpenFactory(path: path ?? '', sqliteOptions: webOptions); } @override Future setupPowerSync( - {String? path, Schema? schema, Logger? logger}) async { + {String? path, + Schema? schema, + Logger? logger, + bool initialize = true}) async { await _isInitialized; - return super.setupPowerSync(path: path, schema: schema); + return super.setupPowerSync( + path: path, schema: schema, logger: logger, initialize: initialize); } @override diff --git a/packages/powersync/test/watch_test.dart b/packages/powersync_core/test/watch_test.dart similarity index 73% rename from packages/powersync/test/watch_test.dart rename to packages/powersync_core/test/watch_test.dart index 493f1aa7..28617e2e 100644 --- a/packages/powersync/test/watch_test.dart +++ b/packages/powersync_core/test/watch_test.dart @@ -1,9 +1,8 @@ -library; - import 'dart:async'; import 'dart:math'; -import 'package:powersync/powersync.dart'; +import 'package:async/async.dart'; +import 'package:powersync_core/powersync_core.dart'; import 'package:sqlite_async/sqlite_async.dart'; import 'package:test/test.dart'; @@ -54,19 +53,19 @@ void main() { 'INSERT INTO customers(id, name) VALUES (?, ?)', [id, 'a customer']); var done = false; - inserts() async { + Future inserts() async { while (!done) { await powersync.execute( 'INSERT INTO assets(id, make, customer_id) VALUES (uuid(), ?, ?)', ['test', id]); - await Future.delayed( + await Future.delayed( Duration(milliseconds: Random().nextInt(baseTime * 2))); } } const numberOfQueries = 10; - inserts(); + final insertsFuture = inserts(); try { List times = []; final results = await stream.take(numberOfQueries).map((e) { @@ -76,7 +75,7 @@ void main() { var lastCount = 0; for (var r in results) { - final count = r.first['count']; + final count = r.first['count'] as int; // This is not strictly incrementing, since we can't guarantee the // exact order between reads and writes. // We can guarantee that there will always be a read after the last write, @@ -85,8 +84,10 @@ void main() { lastCount = count; } - // The number of read queries must not be greater than the number of writes overall. - expect(numberOfQueries, lessThanOrEqualTo(results.last.first['count'])); + // The number of read queries must not be greater than the number of + //writes overall, plus one for an initial read. + expect(numberOfQueries, + lessThanOrEqualTo((results.last.first['count'] as int) + 1)); DateTime? lastTime; for (var r in times) { @@ -99,6 +100,8 @@ void main() { } finally { done = true; } + + await insertsFuture; }); test('onChange', () async { @@ -110,16 +113,16 @@ void main() { const throttleDuration = Duration(milliseconds: baseTime); var done = false; - inserts() async { + Future inserts() async { while (!done) { await powersync.execute( 'INSERT INTO assets(id, make) VALUES (uuid(), ?)', ['test']); - await Future.delayed( + await Future.delayed( Duration(milliseconds: Random().nextInt(baseTime))); } } - inserts(); + final insertsFuture = inserts(); final stream = powersync.onChange({'assets', 'customers'}, throttle: throttleDuration).asyncMap((event) async { @@ -137,6 +140,30 @@ void main() { UpdateNotification.single('assets'), UpdateNotification.single('assets') ])); + await insertsFuture; + }); + + test('emits update events with friendly names', () async { + final powersync = await testUtils.setupPowerSync( + path: path, + schema: Schema([ + Table.localOnly('users', [ + Column.text('name'), + ]), + Table('assets', [ + Column.text('name'), + ]), + ]), + ); + + final updates = StreamQueue(powersync.updates); + await powersync + .execute('INSERT INTO users (id, name) VALUES (uuid(), ?)', ['test']); + await expectLater(updates, emits(UpdateNotification({'users'}))); + + await powersync.execute( + 'INSERT INTO assets (id, name) VALUES (uuid(), ?)', ['test']); + await expectLater(updates, emits(UpdateNotification({'assets'}))); }); }); } diff --git a/packages/powersync_flutter_libs/CHANGELOG.md b/packages/powersync_flutter_libs/CHANGELOG.md index ac0e0449..79e23a87 100644 --- a/packages/powersync_flutter_libs/CHANGELOG.md +++ b/packages/powersync_flutter_libs/CHANGELOG.md @@ -1,3 +1,39 @@ +## 0.4.12 + + - Update core extension, add support for SwiftPM. + +## 0.4.11 + + - Update PowerSync core extension to version 0.4.4. + +## 0.4.10 + + - Update PowerSync core extension to version 0.4.2. + +## 0.4.9 + + - Update PowerSync core extension to version 0.4.0. + +## 0.4.8 + + - Update PowerSync core extension to version 0.3.14. + +## 0.4.7 + + - Update core extension to 0.3.12. + +## 0.4.6 + + - Bump version of core extension to 0.3.11 + +## 0.4.5 + + - Update core extension to 0.3.10 in preparation for bucket priorities. + +## 0.4.4 + + - powersync-sqlite-core 0.3.8 - increases column limit and fixes view migration issue + ## 0.4.3 - powersync-sqlite-core 0.3.6 - fixes dangling rows issue diff --git a/packages/powersync_flutter_libs/README.md b/packages/powersync_flutter_libs/README.md index 19eba4ac..f009332c 100644 --- a/packages/powersync_flutter_libs/README.md +++ b/packages/powersync_flutter_libs/README.md @@ -1,5 +1,5 @@ # powersync_flutter_libs -### Flutter binaries for [PowerSync](https://pub.dev/packages/powersync) please go there for documentation. +### Flutter binaries for [PowerSync](https://pub.dev/packages/powersync). Please go there for documentation. #### The core PowerSync binaries are built and released in [powersync-sqlite-core](https://github.com/powersync-ja/powersync-sqlite-core). diff --git a/packages/powersync_flutter_libs/analysis_options.yaml b/packages/powersync_flutter_libs/analysis_options.yaml index a5744c1c..b49c352c 100644 --- a/packages/powersync_flutter_libs/analysis_options.yaml +++ b/packages/powersync_flutter_libs/analysis_options.yaml @@ -1,4 +1,7 @@ include: package:flutter_lints/flutter.yaml -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true diff --git a/packages/powersync_flutter_libs/android/build.gradle b/packages/powersync_flutter_libs/android/build.gradle index fcbdb6f4..d6c0e3c1 100644 --- a/packages/powersync_flutter_libs/android/build.gradle +++ b/packages/powersync_flutter_libs/android/build.gradle @@ -50,5 +50,5 @@ android { } dependencies { - implementation 'co.powersync:powersync-sqlite-core:0.3.6' + implementation 'com.powersync:powersync-sqlite-core:0.4.6' } diff --git a/packages/powersync_flutter_libs/ios/powersync_flutter_libs.podspec b/packages/powersync_flutter_libs/darwin/powersync_flutter_libs.podspec similarity index 68% rename from packages/powersync_flutter_libs/ios/powersync_flutter_libs.podspec rename to packages/powersync_flutter_libs/darwin/powersync_flutter_libs.podspec index 405fd5ff..b93848a8 100644 --- a/packages/powersync_flutter_libs/ios/powersync_flutter_libs.podspec +++ b/packages/powersync_flutter_libs/darwin/powersync_flutter_libs.podspec @@ -9,20 +9,23 @@ Pod::Spec.new do |s| s.description = <<-DESC A new Flutter FFI plugin project. DESC - s.homepage = 'http://example.com' + s.homepage = 'https://powersync.com' s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } + s.author = { 'Journey Mobile, Inc' => 'hello@powersync.com' } # This will ensure the source files in Classes/ are included in the native # builds of apps using this FFI plugin. Podspec does not support relative # paths, so Classes contains a forwarder C file that relatively imports # `../src/*` so that the C sources can be shared among all target platforms. s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.dependency 'Flutter' - s.platform = :ios, '11.0' + s.source_files = 'powersync_flutter_libs/Sources/powersync_flutter_libs/**/*.swift' + s.ios.dependency 'Flutter' + s.osx.dependency 'FlutterMacOS' + s.ios.deployment_target = '13.0' + s.osx.deployment_target = '10.15' - s.dependency "powersync-sqlite-core", "~> 0.3.6" + # NOTE: Always update Package.swift as well when updating this! + s.dependency "powersync-sqlite-core", "~> 0.4.6" # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } diff --git a/packages/powersync_flutter_libs/darwin/powersync_flutter_libs/Package.resolved b/packages/powersync_flutter_libs/darwin/powersync_flutter_libs/Package.resolved new file mode 100644 index 00000000..a62a2d5c --- /dev/null +++ b/packages/powersync_flutter_libs/darwin/powersync_flutter_libs/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "powersync-sqlite-core-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/powersync-ja/powersync-sqlite-core-swift.git", + "state" : { + "revision" : "b2a81af14e9ad83393eb187bb02e62e6db8b5ad6", + "version" : "0.4.6" + } + } + ], + "version" : 2 +} diff --git a/packages/powersync_flutter_libs/darwin/powersync_flutter_libs/Package.swift b/packages/powersync_flutter_libs/darwin/powersync_flutter_libs/Package.swift new file mode 100644 index 00000000..cfa880df --- /dev/null +++ b/packages/powersync_flutter_libs/darwin/powersync_flutter_libs/Package.swift @@ -0,0 +1,31 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "powersync_flutter_libs", + platforms: [ + .iOS("13.0"), + .macOS("10.15") + ], + products: [ + .library(name: "powersync-flutter-libs", type: .static, targets: ["powersync_flutter_libs"]) + ], + dependencies: [ + .package( + url: "https://github.com/powersync-ja/powersync-sqlite-core-swift.git", + // Note: Always update podspec as well when updating this. + exact: "0.4.6" + ) + ], + targets: [ + .target( + name: "powersync_flutter_libs", + dependencies: [ + .product(name: "PowerSyncSQLiteCore", package: "powersync-sqlite-core-swift") + ], + resources: [] + ) + ] +) diff --git a/packages/powersync_flutter_libs/darwin/powersync_flutter_libs/Sources/powersync_flutter_libs/PowerSyncFlutterLibsPlugin.swift b/packages/powersync_flutter_libs/darwin/powersync_flutter_libs/Sources/powersync_flutter_libs/PowerSyncFlutterLibsPlugin.swift new file mode 100644 index 00000000..f3a14c12 --- /dev/null +++ b/packages/powersync_flutter_libs/darwin/powersync_flutter_libs/Sources/powersync_flutter_libs/PowerSyncFlutterLibsPlugin.swift @@ -0,0 +1,13 @@ +import Foundation + +#if os(iOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#endif + +public class PowersyncFlutterLibsPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + // There's no native code, we just want to link the core extension + } +} diff --git a/packages/powersync_flutter_libs/ios/Classes/PowersyncFlutterLibsPlugin.h b/packages/powersync_flutter_libs/ios/Classes/PowersyncFlutterLibsPlugin.h deleted file mode 100644 index 0ddfb7f7..00000000 --- a/packages/powersync_flutter_libs/ios/Classes/PowersyncFlutterLibsPlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface PowersyncFlutterLibsPlugin : NSObject -@end diff --git a/packages/powersync_flutter_libs/ios/Classes/PowersyncFlutterLibsPlugin.m b/packages/powersync_flutter_libs/ios/Classes/PowersyncFlutterLibsPlugin.m deleted file mode 100644 index 74d2e35c..00000000 --- a/packages/powersync_flutter_libs/ios/Classes/PowersyncFlutterLibsPlugin.m +++ /dev/null @@ -1,7 +0,0 @@ -#import "PowersyncFlutterLibsPlugin.h" - -@implementation PowersyncFlutterLibsPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - -} -@end diff --git a/packages/powersync_flutter_libs/lib/powersync_flutter_libs.dart b/packages/powersync_flutter_libs/lib/powersync_flutter_libs.dart index 4fe98808..7c6f7059 100644 --- a/packages/powersync_flutter_libs/lib/powersync_flutter_libs.dart +++ b/packages/powersync_flutter_libs/lib/powersync_flutter_libs.dart @@ -1,4 +1,4 @@ /// PowerSync Flutter Libs. /// /// This provides binary files for the [PowerSync SQLite Rust Core](https://github.com/powersync-ja/powersync-sqlite-core) -library powersync_flutter_libs; +library; diff --git a/packages/powersync_flutter_libs/macos/Classes/PowersyncFlutterLibsPlugin.h b/packages/powersync_flutter_libs/macos/Classes/PowersyncFlutterLibsPlugin.h deleted file mode 100644 index 9725b1d6..00000000 --- a/packages/powersync_flutter_libs/macos/Classes/PowersyncFlutterLibsPlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface PowersyncFlutterLibsPlugin : NSObject -@end diff --git a/packages/powersync_flutter_libs/macos/Classes/PowersyncFlutterLibsPlugin.m b/packages/powersync_flutter_libs/macos/Classes/PowersyncFlutterLibsPlugin.m deleted file mode 100644 index 74d2e35c..00000000 --- a/packages/powersync_flutter_libs/macos/Classes/PowersyncFlutterLibsPlugin.m +++ /dev/null @@ -1,7 +0,0 @@ -#import "PowersyncFlutterLibsPlugin.h" - -@implementation PowersyncFlutterLibsPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - -} -@end diff --git a/packages/powersync_flutter_libs/macos/powersync_flutter_libs.podspec b/packages/powersync_flutter_libs/macos/powersync_flutter_libs.podspec deleted file mode 100644 index 5130bb3b..00000000 --- a/packages/powersync_flutter_libs/macos/powersync_flutter_libs.podspec +++ /dev/null @@ -1,29 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint powersync_flutter_libs.podspec` to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'powersync_flutter_libs' - s.version = '0.0.1' - s.summary = 'A new Flutter FFI plugin project.' - s.description = <<-DESC -A new Flutter FFI plugin project. - DESC - s.homepage = 'http://example.com' - s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } - - # This will ensure the source files in Classes/ are included in the native - # builds of apps using this FFI plugin. Podspec does not support relative - # paths, so Classes contains a forwarder C file that relatively imports - # `../src/*` so that the C sources can be shared among all target platforms. - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.dependency 'FlutterMacOS' - - s.dependency "powersync-sqlite-core", "~> 0.3.6" - - s.platform = :osx, '10.11' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } - s.swift_version = '5.0' -end diff --git a/packages/powersync_flutter_libs/pubspec.yaml b/packages/powersync_flutter_libs/pubspec.yaml index 125e54dc..a6afea23 100644 --- a/packages/powersync_flutter_libs/pubspec.yaml +++ b/packages/powersync_flutter_libs/pubspec.yaml @@ -1,6 +1,6 @@ name: powersync_flutter_libs description: PowerSync core binaries for the PowerSync Flutter SDK. Needs to be included for Flutter apps. -version: 0.4.3 +version: 0.4.12 repository: https://github.com/powersync-ja/powersync.dart homepage: https://www.powersync.com/ @@ -15,7 +15,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^3.0.0 + flutter_lints: ^5.0.0 flutter: plugin: @@ -25,9 +25,11 @@ flutter: pluginClass: PowersyncFlutterLibsPlugin ios: pluginClass: PowersyncFlutterLibsPlugin - linux: - pluginClass: PowersyncFlutterLibsPlugin + sharedDarwinSource: true macos: pluginClass: PowersyncFlutterLibsPlugin + sharedDarwinSource: true + linux: + pluginClass: PowersyncFlutterLibsPlugin windows: pluginClass: PowersyncFlutterLibsPlugin diff --git a/packages/powersync_sqlcipher/.gitignore b/packages/powersync_sqlcipher/.gitignore new file mode 100644 index 00000000..28f173cc --- /dev/null +++ b/packages/powersync_sqlcipher/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ +/test-db \ No newline at end of file diff --git a/packages/powersync_sqlcipher/.metadata b/packages/powersync_sqlcipher/.metadata new file mode 100644 index 00000000..8f7b6ac1 --- /dev/null +++ b/packages/powersync_sqlcipher/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "761747bfc538b5af34aa0d3fac380f1bc331ec49" + channel: "stable" + +project_type: package diff --git a/packages/powersync_sqlcipher/CHANGELOG.md b/packages/powersync_sqlcipher/CHANGELOG.md new file mode 100644 index 00000000..33b9cb2f --- /dev/null +++ b/packages/powersync_sqlcipher/CHANGELOG.md @@ -0,0 +1,117 @@ +## 0.1.13 + + - Web: Fix decoding sync streams on status. + +## 0.1.12 + +- Add `getCrudTransactions()` returning a stream of completed transactions for uploads. +- Add experimental support for [sync streams](https://docs.powersync.com/usage/sync-streams). +- Add new attachments helper implementation in `package:powersync_core/attachments/attachments.dart`. +- Add SwiftPM support. + +## 0.1.11+1 + + - Fix excessive memory consumption during large sync. + +## 0.1.11 + + - Support latest versions of `package:sqlite3` and `package:sqlite_async`. + - Stream client: Improve `disconnect()` while a connection is being opened. + - Stream client: Support binary sync lines with Rust client and compatible PowerSync service versions. + - Sync client: Improve parsing error responses. + +## 0.1.10 + + - raw tables + +## 0.1.9 + + - Rust client: Fix uploading local writes after reconnect. + - `PowerSyncDatabase.withDatabase`: Rename `loggers` parameter to `logger` for consistency. + - Fix parsing HTTP errors for sync service unavailability. + +## 0.1.8 + +Add a new sync client implementation written in Rust instead of Dart. While +this client is still experimental, we intend to make it the default in the +future. The main benefit of this client is faster sync performance, but +upcoming features will also require this client. +We encourage interested users to try it out by passing `SyncOptions` to the +`connect` method: + +```dart +database.connect( + connector: YourConnector(), + options: const SyncOptions( + syncImplementation: SyncClientImplementation.rust, + ), +); +``` + +Switching between the clients can be done at any time without compatibility +issues. If you run into issues with the new client, please reach out to us! + +## 0.1.7 + + - Allow subclassing open factory for SQLCipher. + +## 0.1.6 + +* Report real-time progress information about downloads through `SyncStatus.downloadProgress`. +* Add `trackPreviousValues` option on `Table` which sets `CrudEntry.previousValues` to previous values on updates. +* Add `trackMetadata` option on `Table` which adds a `_metadata` column that can be used for updates. + The configured metadata is available through `CrudEntry.metadata`. +* Add `ignoreEmptyUpdates` option which skips creating CRUD entries for updates that don't change any values. + +## 0.1.5+4 + + - Update a dependency to the latest release. + +## 0.1.5+3 + +This updates `powersync_core` to version `1.2.3`, which includes these changes: + + - Introduce locks to avoid duplicate sync streams when multiple instances of the same database are opened. + - Refactor connect / disconnect internally. + - Warn when multiple instances of the same database are opened. + - Fix race condition causing data not to be applied while an upload is in progress. + - Web: Fix token invalidation logic when a sync worker is used. + +## 0.1.5+2 + + - Update a dependency to the latest release. + +## 0.1.5+1 + + - Update a dependency to the latest release. + +## 0.1.5 + + - Support bucket priorities and partial syncs. + +## 0.1.4+1 + + - Update a dependency to the latest release. + +## 0.1.4 + + - Web: Support running in contexts where web workers are unavailable. + - Web: Fix sync worker logs not being disabled. + - `powersync_sqlcipher`: Web support. + +## 0.1.3 + + - Fix `statusStream` emitting the same sync status multiple times. + +## 0.1.2 + + - Increase limit on number of columns per table to 1999. + - Avoid deleting the $local bucket on connect(). + +## 0.1.1 + + - Update dependency `powersync_flutter_libs` to v0.4.3 + +## 0.1.0 + + - PowerSync client SDK for Flutter with encryption enabled using SQLCipher initial release diff --git a/packages/powersync_sqlcipher/LICENSE b/packages/powersync_sqlcipher/LICENSE new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/packages/powersync_sqlcipher/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/packages/powersync_sqlcipher/NOTICE b/packages/powersync_sqlcipher/NOTICE new file mode 100644 index 00000000..da0d4ce5 --- /dev/null +++ b/packages/powersync_sqlcipher/NOTICE @@ -0,0 +1 @@ +Copyright 2024 Journey Mobile, Inc. diff --git a/packages/powersync_sqlcipher/README.md b/packages/powersync_sqlcipher/README.md new file mode 100644 index 00000000..3e724919 --- /dev/null +++ b/packages/powersync_sqlcipher/README.md @@ -0,0 +1,64 @@ +

+ +

+ +# PowerSync with SQLCipher SDK for Flutter + +_[PowerSync](https://www.powersync.com) is a sync engine for building local-first apps with instantly-responsive UI/UX and simplified state transfer. Syncs between SQLite on the client-side and Postgres, MongoDB or MySQL on the server-side._ + +This package (`powersync_sqlcipher`) is the PowerSync client SDK for Flutter with encryption enabled using SQLCipher. It is currently in a **beta** release. This means it is safe to use in production, provided that you've tested your use cases. + +If you do not require encryption in your Flutter application, we recommend using the [`powersync`](https://pub.dev/packages/powersync) SDK. + +### Installing PowerSync with SQLCipher encryption in your own project + +Install the latest version of the package, for example: + +```bash +flutter pub add powersync_sqlcipher +``` + +Version history can be found [here](https://pub.dev/packages/powersync_sqlcipher/versions). +To use this package on the web, additional assets are required. You can download them by running this in your project directory: + +``` +dart run powersync_sqlcipher:setup_web +``` + +### Usage + +This SDK requires a slightly different setup to the `powersync` package in order to encrypt the local database: + +```Dart +import 'package/powersync_sqlcipher/powersync.dart'; + +/// Global reference to the database +late final PowerSyncDatabase db; + +final cipherFactory = PowerSyncSQLCipherOpenFactory( + path: path, key: "sqlcipher-encryption-key"); // https://www.zetetic.net/sqlcipher/sqlcipher-api/#key + +db = PowerSyncDatabase.withFactory(cipherFactory, schema: schema); +``` + +# Getting Started + +Our [full SDK reference](https://docs.powersync.com/client-sdk-references/flutter) contains everything you need to know to get started implementing PowerSync in your project. + +# Changelog + +A changelog for this SDK is available [here](https://pub.dev/packages/powersync_sqlcipher/changelog). + +# API Reference + +The full API reference for this SDK can be found [here](https://pub.dev/documentation/powersync_sqlcipher/latest/). + +# Examples + +For example projects built with PowerSync and Flutter, see our [Demo Apps / Example Projects](https://docs.powersync.com/resources/demo-apps-example-projects#flutter) gallery. Most of these projects can also be found in the [`demos/`](../demos/) directory. + +# Found a bug or need help? + +- Join our [Discord server](https://discord.gg/powersync) where you can browse topics from our community, ask questions, share feedback, or just say hello :) +- Please open a [GitHub issue](https://github.com/powersync-ja/powersync.dart/issues) when you come across a bug. +- Have feedback or an idea? [Submit an idea](https://roadmap.powersync.com/tabs/5-roadmap/submit-idea) via our public roadmap or [schedule a chat](https://calendly.com/powersync/powersync-chat) with someone from our product team. diff --git a/packages/powersync_sqlcipher/analysis_options.yaml b/packages/powersync_sqlcipher/analysis_options.yaml new file mode 100644 index 00000000..b49c352c --- /dev/null +++ b/packages/powersync_sqlcipher/analysis_options.yaml @@ -0,0 +1,7 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true diff --git a/packages/powersync_sqlcipher/bin/setup_web.dart b/packages/powersync_sqlcipher/bin/setup_web.dart new file mode 100644 index 00000000..b3509e02 --- /dev/null +++ b/packages/powersync_sqlcipher/bin/setup_web.dart @@ -0,0 +1,4 @@ +// ignore: implementation_imports +import 'package:powersync_core/src/setup_web.dart'; + +void main(List args) => downloadWebAssets(args, encryption: true); diff --git a/packages/powersync_sqlcipher/example/.gitignore b/packages/powersync_sqlcipher/example/.gitignore new file mode 100644 index 00000000..79c113f9 --- /dev/null +++ b/packages/powersync_sqlcipher/example/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/powersync_sqlcipher/example/.metadata b/packages/powersync_sqlcipher/example/.metadata new file mode 100644 index 00000000..c9704a86 --- /dev/null +++ b/packages/powersync_sqlcipher/example/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "be698c48a6750c8cb8e61c740ca9991bb947aba2" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + - platform: android + create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + - platform: ios + create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + - platform: linux + create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + - platform: macos + create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + - platform: web + create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + - platform: windows + create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/powersync_sqlcipher/example/analysis_options.yaml b/packages/powersync_sqlcipher/example/analysis_options.yaml new file mode 100644 index 00000000..0d290213 --- /dev/null +++ b/packages/powersync_sqlcipher/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/powersync_sqlcipher/example/android/.gitignore b/packages/powersync_sqlcipher/example/android/.gitignore new file mode 100644 index 00000000..be3943c9 --- /dev/null +++ b/packages/powersync_sqlcipher/example/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/packages/powersync_sqlcipher/example/android/app/build.gradle.kts b/packages/powersync_sqlcipher/example/android/app/build.gradle.kts new file mode 100644 index 00000000..21ecea97 --- /dev/null +++ b/packages/powersync_sqlcipher/example/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.example" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/packages/powersync_sqlcipher/example/android/app/src/debug/AndroidManifest.xml b/packages/powersync_sqlcipher/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/packages/powersync_sqlcipher/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/powersync_sqlcipher/example/android/app/src/main/AndroidManifest.xml b/packages/powersync_sqlcipher/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..74a78b93 --- /dev/null +++ b/packages/powersync_sqlcipher/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/powersync_sqlcipher/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/packages/powersync_sqlcipher/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt new file mode 100644 index 00000000..ac81bae6 --- /dev/null +++ b/packages/powersync_sqlcipher/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/packages/powersync_sqlcipher/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/powersync_sqlcipher/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/packages/powersync_sqlcipher/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/powersync_sqlcipher/example/android/app/src/main/res/drawable/launch_background.xml b/packages/powersync_sqlcipher/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/packages/powersync_sqlcipher/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/powersync_sqlcipher/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/powersync_sqlcipher/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..db77bb4b Binary files /dev/null and b/packages/powersync_sqlcipher/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/powersync_sqlcipher/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/powersync_sqlcipher/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..17987b79 Binary files /dev/null and b/packages/powersync_sqlcipher/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/powersync_sqlcipher/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/powersync_sqlcipher/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..09d43914 Binary files /dev/null and b/packages/powersync_sqlcipher/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/powersync_sqlcipher/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/powersync_sqlcipher/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..d5f1c8d3 Binary files /dev/null and b/packages/powersync_sqlcipher/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/powersync_sqlcipher/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/powersync_sqlcipher/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..4d6372ee Binary files /dev/null and b/packages/powersync_sqlcipher/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/powersync_sqlcipher/example/android/app/src/main/res/values-night/styles.xml b/packages/powersync_sqlcipher/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..06952be7 --- /dev/null +++ b/packages/powersync_sqlcipher/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/powersync_sqlcipher/example/android/app/src/main/res/values/styles.xml b/packages/powersync_sqlcipher/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..cb1ef880 --- /dev/null +++ b/packages/powersync_sqlcipher/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/powersync_sqlcipher/example/android/app/src/profile/AndroidManifest.xml b/packages/powersync_sqlcipher/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/packages/powersync_sqlcipher/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/powersync_sqlcipher/example/android/build.gradle.kts b/packages/powersync_sqlcipher/example/android/build.gradle.kts new file mode 100644 index 00000000..89176ef4 --- /dev/null +++ b/packages/powersync_sqlcipher/example/android/build.gradle.kts @@ -0,0 +1,21 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/packages/powersync_sqlcipher/example/android/gradle.properties b/packages/powersync_sqlcipher/example/android/gradle.properties new file mode 100644 index 00000000..f018a618 --- /dev/null +++ b/packages/powersync_sqlcipher/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/packages/powersync_sqlcipher/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/powersync_sqlcipher/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..ac3b4792 --- /dev/null +++ b/packages/powersync_sqlcipher/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/packages/powersync_sqlcipher/example/android/settings.gradle.kts b/packages/powersync_sqlcipher/example/android/settings.gradle.kts new file mode 100644 index 00000000..ab39a10a --- /dev/null +++ b/packages/powersync_sqlcipher/example/android/settings.gradle.kts @@ -0,0 +1,25 @@ +pluginManagement { + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.7.3" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false +} + +include(":app") diff --git a/packages/powersync_sqlcipher/example/integration_test/smoke_test.dart b/packages/powersync_sqlcipher/example/integration_test/smoke_test.dart new file mode 100644 index 00000000..1c58fcc7 --- /dev/null +++ b/packages/powersync_sqlcipher/example/integration_test/smoke_test.dart @@ -0,0 +1,72 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:powersync_sqlcipher/powersync.dart'; +import 'package:powersync_sqlcipher/sqlite3_common.dart'; +import 'package:powersync_sqlcipher/sqlite_async.dart'; + +void main() { + test('can use encrypted database', () async { + var path = 'powersync-demo.db'; + // getApplicationSupportDirectory is not supported on Web + if (!kIsWeb) { + final dir = await getApplicationSupportDirectory(); + path = join(dir.path, 'powersync-dart.db'); + } + + var db = PowerSyncDatabase.withFactory( + PowerSyncSQLCipherOpenFactory(path: path, key: 'demo-key'), + schema: schema, + ); + + await db.execute('INSERT INTO users (id, name) VALUES (uuid(), ?)', [ + 'My username', + ]); + await db.close(); + + expect(() async { + db = PowerSyncDatabase.withFactory( + PowerSyncSQLCipherOpenFactory(path: path, key: 'changed-key'), + schema: schema, + ); + + await db.initialize(); + }, throwsA(anything)); + }); + + if (!kIsWeb) { + test('can register user-defined function', () async { + final path = join( + (await getApplicationSupportDirectory()).path, + 'powersync-demo.db', + ); + + final db = PowerSyncDatabase.withFactory( + _CustomOpenFactory(path: path, key: 'demo-key'), + schema: schema, + ); + + await db.get('SELECT my_function()'); + }); + } +} + +final schema = Schema([ + Table('users', [Column.text('name')]), +]); + +final class _CustomOpenFactory extends PowerSyncSQLCipherOpenFactory { + _CustomOpenFactory({required super.path, required super.key}); + + @override + CommonDatabase open(SqliteOpenOptions options) { + final db = super.open(options); + db.createFunction( + functionName: 'my_function', + function: (_) => 123, + argumentCount: AllowedArgumentCount.any(), + ); + return db; + } +} diff --git a/packages/powersync_sqlcipher/example/ios/.gitignore b/packages/powersync_sqlcipher/example/ios/.gitignore new file mode 100644 index 00000000..7a7f9873 --- /dev/null +++ b/packages/powersync_sqlcipher/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/packages/powersync_sqlcipher/example/ios/Flutter/AppFrameworkInfo.plist b/packages/powersync_sqlcipher/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 00000000..7c569640 --- /dev/null +++ b/packages/powersync_sqlcipher/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/packages/powersync_sqlcipher/example/ios/Flutter/Debug.xcconfig b/packages/powersync_sqlcipher/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 00000000..ec97fc6f --- /dev/null +++ b/packages/powersync_sqlcipher/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/powersync_sqlcipher/example/ios/Flutter/Release.xcconfig b/packages/powersync_sqlcipher/example/ios/Flutter/Release.xcconfig new file mode 100644 index 00000000..c4855bfe --- /dev/null +++ b/packages/powersync_sqlcipher/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/powersync_sqlcipher/example/ios/Podfile b/packages/powersync_sqlcipher/example/ios/Podfile new file mode 100644 index 00000000..620e46eb --- /dev/null +++ b/packages/powersync_sqlcipher/example/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/powersync_sqlcipher/example/ios/Podfile.lock b/packages/powersync_sqlcipher/example/ios/Podfile.lock new file mode 100644 index 00000000..9a83253f --- /dev/null +++ b/packages/powersync_sqlcipher/example/ios/Podfile.lock @@ -0,0 +1,56 @@ +PODS: + - Flutter (1.0.0) + - integration_test (0.0.1): + - Flutter + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - powersync-sqlite-core (0.3.14) + - powersync_flutter_libs (0.0.1): + - Flutter + - powersync-sqlite-core (~> 0.3.14) + - SQLCipher (4.8.0): + - SQLCipher/standard (= 4.8.0) + - SQLCipher/common (4.8.0) + - SQLCipher/standard (4.8.0): + - SQLCipher/common + - sqlcipher_flutter_libs (0.0.1): + - Flutter + - SQLCipher (~> 4.8.0) + +DEPENDENCIES: + - Flutter (from `Flutter`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - powersync_flutter_libs (from `.symlinks/plugins/powersync_flutter_libs/ios`) + - sqlcipher_flutter_libs (from `.symlinks/plugins/sqlcipher_flutter_libs/ios`) + +SPEC REPOS: + trunk: + - powersync-sqlite-core + - SQLCipher + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + integration_test: + :path: ".symlinks/plugins/integration_test/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + powersync_flutter_libs: + :path: ".symlinks/plugins/powersync_flutter_libs/ios" + sqlcipher_flutter_libs: + :path: ".symlinks/plugins/sqlcipher_flutter_libs/ios" + +SPEC CHECKSUMS: + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + powersync-sqlite-core: ef06642c8110680fcddce8a8c0dd2696daaf672d + powersync_flutter_libs: a3d3b06267aa2d86c9dfc1fb8fb0b1136859b0c4 + SQLCipher: 908f846ca79d74be4e1776b3b86c6ad9e6c0b04f + sqlcipher_flutter_libs: 3679469b27cd0599ae424605add6638f605e800e + +PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5 + +COCOAPODS: 1.16.2 diff --git a/packages/powersync_sqlcipher/example/ios/Runner.xcodeproj/project.pbxproj b/packages/powersync_sqlcipher/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..0fb8dea6 --- /dev/null +++ b/packages/powersync_sqlcipher/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,731 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 89F8B3A69F2C68E6B205A782 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E8F04E84F5BCA065F714D4FA /* Pods_Runner.framework */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + F93D6AD2E55C7F3787562F0E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4FA94ED98DF3BD260AB31002 /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0F1E095741678A78DCFDF937 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1F6922DC184523B3167B0169 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 4FA94ED98DF3BD260AB31002 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5EE6431A1CCED8FF25DC684D /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 6A9D7DBC049EE1EC33716814 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + C0A1775467751D8A14ABE969 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + C2E4CA9F95DE8D954EEB2901 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + E8F04E84F5BCA065F714D4FA /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 301EF62EE309E1EB94E864EA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F93D6AD2E55C7F3787562F0E /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 89F8B3A69F2C68E6B205A782 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + D2194BCDE4E5D69D3419036E /* Pods */, + E1AF1D4991C390CC108EC6B3 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + D2194BCDE4E5D69D3419036E /* Pods */ = { + isa = PBXGroup; + children = ( + 0F1E095741678A78DCFDF937 /* Pods-Runner.debug.xcconfig */, + C0A1775467751D8A14ABE969 /* Pods-Runner.release.xcconfig */, + 1F6922DC184523B3167B0169 /* Pods-Runner.profile.xcconfig */, + C2E4CA9F95DE8D954EEB2901 /* Pods-RunnerTests.debug.xcconfig */, + 5EE6431A1CCED8FF25DC684D /* Pods-RunnerTests.release.xcconfig */, + 6A9D7DBC049EE1EC33716814 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + E1AF1D4991C390CC108EC6B3 /* Frameworks */ = { + isa = PBXGroup; + children = ( + E8F04E84F5BCA065F714D4FA /* Pods_Runner.framework */, + 4FA94ED98DF3BD260AB31002 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + D9EB2E87EF6E9DF19DB6738D /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 301EF62EE309E1EB94E864EA /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + DC9F2AD629E79D6110BC6CCA /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + B4BA2594869B4C585C1F16E5 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + B4BA2594869B4C585C1F16E5 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + D9EB2E87EF6E9DF19DB6738D /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + DC9F2AD629E79D6110BC6CCA /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 39T7JMB94Q; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C2E4CA9F95DE8D954EEB2901 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5EE6431A1CCED8FF25DC684D /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6A9D7DBC049EE1EC33716814 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 39T7JMB94Q; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 39T7JMB94Q; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/powersync_sqlcipher/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/powersync_sqlcipher/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/packages/powersync_sqlcipher/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/powersync_sqlcipher/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/powersync_sqlcipher/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/packages/powersync_sqlcipher/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/powersync_sqlcipher/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/powersync_sqlcipher/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/packages/powersync_sqlcipher/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/powersync_sqlcipher/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/powersync_sqlcipher/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..e3773d42 --- /dev/null +++ b/packages/powersync_sqlcipher/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/powersync_sqlcipher/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/powersync_sqlcipher/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/packages/powersync_sqlcipher/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/powersync_sqlcipher/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/powersync_sqlcipher/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/packages/powersync_sqlcipher/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/powersync_sqlcipher/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/powersync_sqlcipher/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/packages/powersync_sqlcipher/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/powersync_sqlcipher/example/ios/Runner/AppDelegate.swift b/packages/powersync_sqlcipher/example/ios/Runner/AppDelegate.swift new file mode 100644 index 00000000..62666446 --- /dev/null +++ b/packages/powersync_sqlcipher/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d36b1fab --- /dev/null +++ b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 00000000..dc9ada47 Binary files /dev/null and b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 00000000..7353c41e Binary files /dev/null and b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 00000000..797d452e Binary files /dev/null and b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 00000000..6ed2d933 Binary files /dev/null and b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 00000000..4cd7b009 Binary files /dev/null and b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 00000000..fe730945 Binary files /dev/null and b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 00000000..321773cd Binary files /dev/null and b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 00000000..797d452e Binary files /dev/null and b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 00000000..502f463a Binary files /dev/null and b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 00000000..0ec30343 Binary files /dev/null and b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 00000000..0ec30343 Binary files /dev/null and b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 00000000..e9f5fea2 Binary files /dev/null and b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 00000000..84ac32ae Binary files /dev/null and b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 00000000..8953cba0 Binary files /dev/null and b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 00000000..0467bf12 Binary files /dev/null and b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 00000000..0bedcf2f --- /dev/null +++ b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 00000000..89c2725b --- /dev/null +++ b/packages/powersync_sqlcipher/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/powersync_sqlcipher/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/powersync_sqlcipher/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..f2e259c7 --- /dev/null +++ b/packages/powersync_sqlcipher/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/powersync_sqlcipher/example/ios/Runner/Base.lproj/Main.storyboard b/packages/powersync_sqlcipher/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f3c28516 --- /dev/null +++ b/packages/powersync_sqlcipher/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/powersync_sqlcipher/example/ios/Runner/Info.plist b/packages/powersync_sqlcipher/example/ios/Runner/Info.plist new file mode 100644 index 00000000..5458fc41 --- /dev/null +++ b/packages/powersync_sqlcipher/example/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/packages/powersync_sqlcipher/example/ios/Runner/Runner-Bridging-Header.h b/packages/powersync_sqlcipher/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 00000000..308a2a56 --- /dev/null +++ b/packages/powersync_sqlcipher/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/packages/powersync_sqlcipher/example/ios/RunnerTests/RunnerTests.swift b/packages/powersync_sqlcipher/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..86a7c3b1 --- /dev/null +++ b/packages/powersync_sqlcipher/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/packages/powersync_sqlcipher/example/lib/main.dart b/packages/powersync_sqlcipher/example/lib/main.dart new file mode 100644 index 00000000..eb7938d3 --- /dev/null +++ b/packages/powersync_sqlcipher/example/lib/main.dart @@ -0,0 +1,54 @@ +import 'package:flutter/foundation.dart'; +import 'package:powersync_sqlcipher/powersync.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart'; + +const schema = Schema([ + Table('customers', [Column.text('name'), Column.text('email')]), +]); + +late PowerSyncDatabase db; + +// Setup connector to backend if you would like to sync data. +class BackendConnector extends PowerSyncBackendConnector { + PowerSyncDatabase db; + + BackendConnector(this.db); + @override + // ignore: body_might_complete_normally_nullable + Future fetchCredentials() async { + // implement fetchCredentials + } + @override + Future uploadData(PowerSyncDatabase database) async { + // implement uploadData + } +} + +Future openDatabase() async { + var path = 'powersync-demo.db'; + // getApplicationSupportDirectory is not supported on Web + if (!kIsWeb) { + final dir = await getApplicationSupportDirectory(); + path = join(dir.path, 'powersync-dart.db'); + } + + // Setup the database. + final cipherFactory = PowerSyncSQLCipherOpenFactory( + path: path, + key: "sqlcipher-encryption-key", + ); + + db = PowerSyncDatabase.withFactory(cipherFactory, schema: schema); + + await db.initialize(); + + // Run local statements. + await db.execute( + 'INSERT INTO customers(id, name, email) VALUES(uuid(), ?, ?)', + ['Fred', 'fred@example.org'], + ); + + // Connect to backend + db.connect(connector: BackendConnector(db)); +} diff --git a/packages/powersync_sqlcipher/example/linux/.gitignore b/packages/powersync_sqlcipher/example/linux/.gitignore new file mode 100644 index 00000000..d3896c98 --- /dev/null +++ b/packages/powersync_sqlcipher/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/packages/powersync_sqlcipher/example/linux/CMakeLists.txt b/packages/powersync_sqlcipher/example/linux/CMakeLists.txt new file mode 100644 index 00000000..7a9a314f --- /dev/null +++ b/packages/powersync_sqlcipher/example/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "example") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/packages/powersync_sqlcipher/example/linux/flutter/CMakeLists.txt b/packages/powersync_sqlcipher/example/linux/flutter/CMakeLists.txt new file mode 100644 index 00000000..d5bd0164 --- /dev/null +++ b/packages/powersync_sqlcipher/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/packages/powersync_sqlcipher/example/linux/flutter/generated_plugin_registrant.cc b/packages/powersync_sqlcipher/example/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..db04f028 --- /dev/null +++ b/packages/powersync_sqlcipher/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,19 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) powersync_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "PowersyncFlutterLibsPlugin"); + powersync_flutter_libs_plugin_register_with_registrar(powersync_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) sqlcipher_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); + sqlite3_flutter_libs_plugin_register_with_registrar(sqlcipher_flutter_libs_registrar); +} diff --git a/packages/powersync_sqlcipher/example/linux/flutter/generated_plugin_registrant.h b/packages/powersync_sqlcipher/example/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..e0f0a47b --- /dev/null +++ b/packages/powersync_sqlcipher/example/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/powersync_sqlcipher/example/linux/flutter/generated_plugins.cmake b/packages/powersync_sqlcipher/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 00000000..504da4ec --- /dev/null +++ b/packages/powersync_sqlcipher/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,25 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + powersync_flutter_libs + sqlcipher_flutter_libs +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/powersync_sqlcipher/example/linux/runner/CMakeLists.txt b/packages/powersync_sqlcipher/example/linux/runner/CMakeLists.txt new file mode 100644 index 00000000..e97dabc7 --- /dev/null +++ b/packages/powersync_sqlcipher/example/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/packages/powersync_sqlcipher/example/linux/runner/main.cc b/packages/powersync_sqlcipher/example/linux/runner/main.cc new file mode 100644 index 00000000..e7c5c543 --- /dev/null +++ b/packages/powersync_sqlcipher/example/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/packages/powersync_sqlcipher/example/linux/runner/my_application.cc b/packages/powersync_sqlcipher/example/linux/runner/my_application.cc new file mode 100644 index 00000000..6c810823 --- /dev/null +++ b/packages/powersync_sqlcipher/example/linux/runner/my_application.cc @@ -0,0 +1,130 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/packages/powersync_sqlcipher/example/linux/runner/my_application.h b/packages/powersync_sqlcipher/example/linux/runner/my_application.h new file mode 100644 index 00000000..72271d5e --- /dev/null +++ b/packages/powersync_sqlcipher/example/linux/runner/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/packages/powersync_sqlcipher/example/macos/.gitignore b/packages/powersync_sqlcipher/example/macos/.gitignore new file mode 100644 index 00000000..746adbb6 --- /dev/null +++ b/packages/powersync_sqlcipher/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/packages/powersync_sqlcipher/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/powersync_sqlcipher/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 00000000..4b81f9b2 --- /dev/null +++ b/packages/powersync_sqlcipher/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/powersync_sqlcipher/example/macos/Flutter/Flutter-Release.xcconfig b/packages/powersync_sqlcipher/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 00000000..5caa9d15 --- /dev/null +++ b/packages/powersync_sqlcipher/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/powersync_sqlcipher/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/powersync_sqlcipher/example/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 00000000..71be4464 --- /dev/null +++ b/packages/powersync_sqlcipher/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,16 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import path_provider_foundation +import powersync_flutter_libs +import sqlcipher_flutter_libs + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + PowersyncFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "PowersyncFlutterLibsPlugin")) + Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) +} diff --git a/packages/powersync_sqlcipher/example/macos/Podfile b/packages/powersync_sqlcipher/example/macos/Podfile new file mode 100644 index 00000000..ff5ddb3b --- /dev/null +++ b/packages/powersync_sqlcipher/example/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/powersync_sqlcipher/example/macos/Podfile.lock b/packages/powersync_sqlcipher/example/macos/Podfile.lock new file mode 100644 index 00000000..4200a777 --- /dev/null +++ b/packages/powersync_sqlcipher/example/macos/Podfile.lock @@ -0,0 +1,50 @@ +PODS: + - FlutterMacOS (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - powersync-sqlite-core (0.3.14) + - powersync_flutter_libs (0.0.1): + - FlutterMacOS + - powersync-sqlite-core (~> 0.3.14) + - SQLCipher (4.8.0): + - SQLCipher/standard (= 4.8.0) + - SQLCipher/common (4.8.0) + - SQLCipher/standard (4.8.0): + - SQLCipher/common + - sqlcipher_flutter_libs (0.0.1): + - FlutterMacOS + - SQLCipher (~> 4.8.0) + +DEPENDENCIES: + - FlutterMacOS (from `Flutter/ephemeral`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - powersync_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/powersync_flutter_libs/macos`) + - sqlcipher_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlcipher_flutter_libs/macos`) + +SPEC REPOS: + trunk: + - powersync-sqlite-core + - SQLCipher + +EXTERNAL SOURCES: + FlutterMacOS: + :path: Flutter/ephemeral + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + powersync_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/powersync_flutter_libs/macos + sqlcipher_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/sqlcipher_flutter_libs/macos + +SPEC CHECKSUMS: + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + powersync-sqlite-core: ef06642c8110680fcddce8a8c0dd2696daaf672d + powersync_flutter_libs: 700af2a44bae66ae6bf79ed4cf1fefe379735936 + SQLCipher: 908f846ca79d74be4e1776b3b86c6ad9e6c0b04f + sqlcipher_flutter_libs: 72569ed27a3f8d3502571be15fdc3e28f8f8570c + +PODFILE CHECKSUM: 7eb978b976557c8c1cd717d8185ec483fd090a82 + +COCOAPODS: 1.16.2 diff --git a/packages/powersync_sqlcipher/example/macos/Runner.xcodeproj/project.pbxproj b/packages/powersync_sqlcipher/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..ce58f2c2 --- /dev/null +++ b/packages/powersync_sqlcipher/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,801 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 396E1DD957708B96FBEBF2CC /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFE160E4FE814ADF0EE3E6C6 /* Pods_Runner.framework */; }; + 4CB11B6A52B66FC88F44B5D3 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 64B139C34F88049EB8B367D6 /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0584B59685E92EBDF57974EC /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 149B49C5DA258CE54B1B8ED3 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 5CCCF63E56EE3517CA80293B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 64B139C34F88049EB8B367D6 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 682128308F28C2598F44267D /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 7C6CF6CD3F4F0A7FC2553FF3 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + BFE160E4FE814ADF0EE3E6C6 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C3F15032819B9061B9D10037 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4CB11B6A52B66FC88F44B5D3 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 396E1DD957708B96FBEBF2CC /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 8C875D0F08B226532B826246 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* example.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 8C875D0F08B226532B826246 /* Pods */ = { + isa = PBXGroup; + children = ( + C3F15032819B9061B9D10037 /* Pods-Runner.debug.xcconfig */, + 0584B59685E92EBDF57974EC /* Pods-Runner.release.xcconfig */, + 682128308F28C2598F44267D /* Pods-Runner.profile.xcconfig */, + 7C6CF6CD3F4F0A7FC2553FF3 /* Pods-RunnerTests.debug.xcconfig */, + 5CCCF63E56EE3517CA80293B /* Pods-RunnerTests.release.xcconfig */, + 149B49C5DA258CE54B1B8ED3 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + BFE160E4FE814ADF0EE3E6C6 /* Pods_Runner.framework */, + 64B139C34F88049EB8B367D6 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 2DB411C6D486F855A4E19F9D /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 23FAE180B290889E4031D356 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 0124508008F4E40D8D03FB9A /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0124508008F4E40D8D03FB9A /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 23FAE180B290889E4031D356 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 2DB411C6D486F855A4E19F9D /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7C6CF6CD3F4F0A7FC2553FF3 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5CCCF63E56EE3517CA80293B /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 149B49C5DA258CE54B1B8ED3 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/packages/powersync_sqlcipher/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/powersync_sqlcipher/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/packages/powersync_sqlcipher/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/powersync_sqlcipher/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/powersync_sqlcipher/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..ac78810c --- /dev/null +++ b/packages/powersync_sqlcipher/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/powersync_sqlcipher/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/powersync_sqlcipher/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/packages/powersync_sqlcipher/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/powersync_sqlcipher/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/powersync_sqlcipher/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/packages/powersync_sqlcipher/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/powersync_sqlcipher/example/macos/Runner/AppDelegate.swift b/packages/powersync_sqlcipher/example/macos/Runner/AppDelegate.swift new file mode 100644 index 00000000..b3c17614 --- /dev/null +++ b/packages/powersync_sqlcipher/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/packages/powersync_sqlcipher/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/powersync_sqlcipher/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..a2ec33f1 --- /dev/null +++ b/packages/powersync_sqlcipher/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/powersync_sqlcipher/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/powersync_sqlcipher/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 00000000..82b6f9d9 Binary files /dev/null and b/packages/powersync_sqlcipher/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/packages/powersync_sqlcipher/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/powersync_sqlcipher/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 00000000..13b35eba Binary files /dev/null and b/packages/powersync_sqlcipher/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/packages/powersync_sqlcipher/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/powersync_sqlcipher/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 00000000..0a3f5fa4 Binary files /dev/null and b/packages/powersync_sqlcipher/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/packages/powersync_sqlcipher/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/powersync_sqlcipher/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 00000000..bdb57226 Binary files /dev/null and b/packages/powersync_sqlcipher/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/packages/powersync_sqlcipher/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/powersync_sqlcipher/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 00000000..f083318e Binary files /dev/null and b/packages/powersync_sqlcipher/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/packages/powersync_sqlcipher/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/powersync_sqlcipher/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 00000000..326c0e72 Binary files /dev/null and b/packages/powersync_sqlcipher/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/packages/powersync_sqlcipher/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/powersync_sqlcipher/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 00000000..2f1632cf Binary files /dev/null and b/packages/powersync_sqlcipher/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/packages/powersync_sqlcipher/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/powersync_sqlcipher/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 00000000..80e867a4 --- /dev/null +++ b/packages/powersync_sqlcipher/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/powersync_sqlcipher/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/powersync_sqlcipher/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 00000000..dda9752f --- /dev/null +++ b/packages/powersync_sqlcipher/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.example + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/packages/powersync_sqlcipher/example/macos/Runner/Configs/Debug.xcconfig b/packages/powersync_sqlcipher/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 00000000..36b0fd94 --- /dev/null +++ b/packages/powersync_sqlcipher/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/powersync_sqlcipher/example/macos/Runner/Configs/Release.xcconfig b/packages/powersync_sqlcipher/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 00000000..dff4f495 --- /dev/null +++ b/packages/powersync_sqlcipher/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/powersync_sqlcipher/example/macos/Runner/Configs/Warnings.xcconfig b/packages/powersync_sqlcipher/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 00000000..42bcbf47 --- /dev/null +++ b/packages/powersync_sqlcipher/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/packages/powersync_sqlcipher/example/macos/Runner/DebugProfile.entitlements b/packages/powersync_sqlcipher/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 00000000..dddb8a30 --- /dev/null +++ b/packages/powersync_sqlcipher/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/packages/powersync_sqlcipher/example/macos/Runner/Info.plist b/packages/powersync_sqlcipher/example/macos/Runner/Info.plist new file mode 100644 index 00000000..4789daa6 --- /dev/null +++ b/packages/powersync_sqlcipher/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/packages/powersync_sqlcipher/example/macos/Runner/MainFlutterWindow.swift b/packages/powersync_sqlcipher/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 00000000..3cc05eb2 --- /dev/null +++ b/packages/powersync_sqlcipher/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/packages/powersync_sqlcipher/example/macos/Runner/Release.entitlements b/packages/powersync_sqlcipher/example/macos/Runner/Release.entitlements new file mode 100644 index 00000000..852fa1a4 --- /dev/null +++ b/packages/powersync_sqlcipher/example/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/packages/powersync_sqlcipher/example/macos/RunnerTests/RunnerTests.swift b/packages/powersync_sqlcipher/example/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..61f3bd1f --- /dev/null +++ b/packages/powersync_sqlcipher/example/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/packages/powersync_sqlcipher/example/pubspec.lock b/packages/powersync_sqlcipher/example/pubspec.lock new file mode 100644 index 00000000..e43cc1be --- /dev/null +++ b/packages/powersync_sqlcipher/example/pubspec.lock @@ -0,0 +1,553 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + http: + dependency: transitive + description: + name: http + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + url: "https://pub.dev" + source: hosted + version: "10.0.9" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.dev" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mutex: + dependency: transitive + description: + name: mutex + sha256: "8827da25de792088eb33e572115a5eb0d61d61a3c01acbc8bcbe76ed78f1a1f2" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + path: + dependency: "direct main" + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + url: "https://pub.dev" + source: hosted + version: "2.2.17" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + powersync_core: + dependency: "direct overridden" + description: + path: "../../powersync_core" + relative: true + source: path + version: "1.5.0" + powersync_flutter_libs: + dependency: "direct overridden" + description: + path: "../../powersync_flutter_libs" + relative: true + source: path + version: "0.4.10" + powersync_sqlcipher: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.1.10" + process: + dependency: transitive + description: + name: process + sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + shelf: + dependency: "direct dev" + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_static: + dependency: "direct dev" + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqlcipher_flutter_libs: + dependency: transitive + description: + name: sqlcipher_flutter_libs + sha256: "777c3469ada8fe6b808bd50f1c752cdd2ca1b1f3cf751d434502ead15334f3a5" + url: "https://pub.dev" + source: hosted + version: "0.6.6" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: c0503c69b44d5714e6abbf4c1f51a3c3cc42b75ce785f44404765e4635481d38 + url: "https://pub.dev" + source: hosted + version: "2.7.6" + sqlite3_web: + dependency: transitive + description: + name: sqlite3_web + sha256: "967e076442f7e1233bd7241ca61f3efe4c7fc168dac0f38411bdb3bdf471eb3c" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + sqlite_async: + dependency: transitive + description: + name: sqlite_async + sha256: "85c0bcabee382fe46a52d6343b9b41f58c0a4d4b0245f55c223c9aa008c1d675" + url: "https://pub.dev" + source: hosted + version: "0.11.5" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: "direct dev" + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.dev" + source: hosted + version: "0.7.4" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + url: "https://pub.dev" + source: hosted + version: "15.0.0" + web: + dependency: "direct dev" + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.8.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/packages/powersync_sqlcipher/example/pubspec.yaml b/packages/powersync_sqlcipher/example/pubspec.yaml new file mode 100644 index 00000000..753aa73a --- /dev/null +++ b/packages/powersync_sqlcipher/example/pubspec.yaml @@ -0,0 +1,31 @@ +name: powersync_sqlcipher_example +description: "Example and integration tests for powersync SQLCipher" +publish_to: 'none' + +version: 1.0.0+1 + +environment: + sdk: ^3.8.0 + +dependencies: + flutter: + sdk: flutter + + path: ^1.9.1 + path_provider: ^2.1.5 + powersync_sqlcipher: ^0.1.13 + +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + + flutter_lints: ^5.0.0 + shelf: ^1.4.2 + shelf_static: ^1.1.3 + stream_channel: ^2.1.4 + web: ^1.1.1 + +flutter: + uses-material-design: true diff --git a/packages/powersync_sqlcipher/example/test/web_test.dart b/packages/powersync_sqlcipher/example/test/web_test.dart new file mode 100644 index 00000000..d81c4c6c --- /dev/null +++ b/packages/powersync_sqlcipher/example/test/web_test.dart @@ -0,0 +1,70 @@ +@TestOn('js') +library; + +import 'dart:js_interop'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:powersync_sqlcipher/powersync.dart'; +import 'package:powersync_sqlcipher/sqlite_async.dart'; +import 'package:web/web.dart' as web; + +void main() { + // We can't run integration tests on the web, so this is a small smoke test + // using the worker + test('can use encrypted database', () async { + final channel = spawnHybridUri('/test/worker_server.dart'); + final port = (await channel.stream.first as num).toInt(); + final sqliteWasmUri = 'http://localhost:$port/sqlite3mc.wasm'; + // Cross origin workers are not supported, but we can supply a Blob + var sqliteUri = 'http://localhost:$port/db_worker.js'; + + final blob = web.Blob( + ['importScripts("$sqliteUri");'.toJS].toJS, + web.BlobPropertyBag(type: 'application/javascript'), + ); + sqliteUri = _createObjectURL(blob); + + final webOptions = SqliteOptions( + webSqliteOptions: WebSqliteOptions( + wasmUri: sqliteWasmUri.toString(), + workerUri: sqliteUri, + ), + ); + + final path = 'powersync-demo.db'; + + var db = PowerSyncDatabase.withFactory( + PowerSyncSQLCipherOpenFactory( + path: path, + key: 'demo-key', + sqliteOptions: webOptions, + ), + schema: schema, + ); + + await db.execute('INSERT INTO users (id, name) VALUES (uuid(), ?)', [ + 'My username', + ]); + await db.close(); + + expect(() async { + db = PowerSyncDatabase.withFactory( + PowerSyncSQLCipherOpenFactory( + path: path, + key: 'changed-key', + sqliteOptions: webOptions, + ), + schema: schema, + ); + + await db.initialize(); + }, throwsA(anything)); + }); +} + +@JS('URL.createObjectURL') +external String _createObjectURL(web.Blob blob); + +final schema = Schema([ + Table('users', [Column.text('name')]), +]); diff --git a/packages/powersync_sqlcipher/example/test/worker_server.dart b/packages/powersync_sqlcipher/example/test/worker_server.dart new file mode 100644 index 00000000..af56a5f1 --- /dev/null +++ b/packages/powersync_sqlcipher/example/test/worker_server.dart @@ -0,0 +1,51 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:shelf/shelf.dart'; +import 'package:shelf/shelf_io.dart' as io; +import 'package:shelf_static/shelf_static.dart'; +import 'package:stream_channel/stream_channel.dart'; + +const _corsHeaders = {'Access-Control-Allow-Origin': '*'}; + +Middleware cors() { + Response? handleOptionsRequest(Request request) { + if (request.method == 'OPTIONS') { + return Response.ok(null, headers: _corsHeaders); + } else { + // Returning null will run the regular request handler + return null; + } + } + + Response addCorsHeaders(Response response) { + return response.change(headers: _corsHeaders); + } + + return createMiddleware( + requestHandler: handleOptionsRequest, + responseHandler: addCorsHeaders, + ); +} + +Future hybridMain(StreamChannel channel) async { + final sqliteOutputPath = p.join('web', 'sqlite3mc.wasm'); + + if (!(await File(sqliteOutputPath).exists())) { + throw AssertionError( + 'sqlite3mc.wasm file should be present in the ./web folder', + ); + } + + final server = await HttpServer.bind('localhost', 0); + + final handler = const Pipeline() + .addMiddleware(cors()) + .addHandler(createStaticHandler('web')); + io.serveRequests(server, handler); + + channel.sink.add(server.port); + await channel.stream.listen(null).asFuture().then((_) async { + await server.close(); + }); +} diff --git a/packages/powersync_sqlcipher/example/web/favicon.png b/packages/powersync_sqlcipher/example/web/favicon.png new file mode 100644 index 00000000..8aaa46ac Binary files /dev/null and b/packages/powersync_sqlcipher/example/web/favicon.png differ diff --git a/packages/powersync_sqlcipher/example/web/icons/Icon-192.png b/packages/powersync_sqlcipher/example/web/icons/Icon-192.png new file mode 100644 index 00000000..b749bfef Binary files /dev/null and b/packages/powersync_sqlcipher/example/web/icons/Icon-192.png differ diff --git a/packages/powersync_sqlcipher/example/web/icons/Icon-512.png b/packages/powersync_sqlcipher/example/web/icons/Icon-512.png new file mode 100644 index 00000000..88cfd48d Binary files /dev/null and b/packages/powersync_sqlcipher/example/web/icons/Icon-512.png differ diff --git a/packages/powersync_sqlcipher/example/web/icons/Icon-maskable-192.png b/packages/powersync_sqlcipher/example/web/icons/Icon-maskable-192.png new file mode 100644 index 00000000..eb9b4d76 Binary files /dev/null and b/packages/powersync_sqlcipher/example/web/icons/Icon-maskable-192.png differ diff --git a/packages/powersync_sqlcipher/example/web/icons/Icon-maskable-512.png b/packages/powersync_sqlcipher/example/web/icons/Icon-maskable-512.png new file mode 100644 index 00000000..d69c5669 Binary files /dev/null and b/packages/powersync_sqlcipher/example/web/icons/Icon-maskable-512.png differ diff --git a/packages/powersync_sqlcipher/example/web/index.html b/packages/powersync_sqlcipher/example/web/index.html new file mode 100644 index 00000000..29b58086 --- /dev/null +++ b/packages/powersync_sqlcipher/example/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + example + + + + + + diff --git a/packages/powersync_sqlcipher/example/web/manifest.json b/packages/powersync_sqlcipher/example/web/manifest.json new file mode 100644 index 00000000..096edf8f --- /dev/null +++ b/packages/powersync_sqlcipher/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/packages/powersync_sqlcipher/example/windows/.gitignore b/packages/powersync_sqlcipher/example/windows/.gitignore new file mode 100644 index 00000000..d492d0d9 --- /dev/null +++ b/packages/powersync_sqlcipher/example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/packages/powersync_sqlcipher/example/windows/CMakeLists.txt b/packages/powersync_sqlcipher/example/windows/CMakeLists.txt new file mode 100644 index 00000000..d960948a --- /dev/null +++ b/packages/powersync_sqlcipher/example/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(example LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/packages/powersync_sqlcipher/example/windows/flutter/CMakeLists.txt b/packages/powersync_sqlcipher/example/windows/flutter/CMakeLists.txt new file mode 100644 index 00000000..903f4899 --- /dev/null +++ b/packages/powersync_sqlcipher/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/packages/powersync_sqlcipher/example/windows/flutter/generated_plugin_registrant.cc b/packages/powersync_sqlcipher/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..67387be8 --- /dev/null +++ b/packages/powersync_sqlcipher/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,17 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + PowersyncFlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PowersyncFlutterLibsPlugin")); + Sqlite3FlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); +} diff --git a/packages/powersync_sqlcipher/example/windows/flutter/generated_plugin_registrant.h b/packages/powersync_sqlcipher/example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..dc139d85 --- /dev/null +++ b/packages/powersync_sqlcipher/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/powersync_sqlcipher/example/windows/flutter/generated_plugins.cmake b/packages/powersync_sqlcipher/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 00000000..df74ce22 --- /dev/null +++ b/packages/powersync_sqlcipher/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,25 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + powersync_flutter_libs + sqlcipher_flutter_libs +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/powersync_sqlcipher/example/windows/runner/CMakeLists.txt b/packages/powersync_sqlcipher/example/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..394917c0 --- /dev/null +++ b/packages/powersync_sqlcipher/example/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/packages/powersync_sqlcipher/example/windows/runner/Runner.rc b/packages/powersync_sqlcipher/example/windows/runner/Runner.rc new file mode 100644 index 00000000..289fc7ee --- /dev/null +++ b/packages/powersync_sqlcipher/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "example.exe" "\0" + VALUE "ProductName", "example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/packages/powersync_sqlcipher/example/windows/runner/flutter_window.cpp b/packages/powersync_sqlcipher/example/windows/runner/flutter_window.cpp new file mode 100644 index 00000000..955ee303 --- /dev/null +++ b/packages/powersync_sqlcipher/example/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/packages/powersync_sqlcipher/example/windows/runner/flutter_window.h b/packages/powersync_sqlcipher/example/windows/runner/flutter_window.h new file mode 100644 index 00000000..6da0652f --- /dev/null +++ b/packages/powersync_sqlcipher/example/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/packages/powersync_sqlcipher/example/windows/runner/main.cpp b/packages/powersync_sqlcipher/example/windows/runner/main.cpp new file mode 100644 index 00000000..a61bf80d --- /dev/null +++ b/packages/powersync_sqlcipher/example/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/packages/powersync_sqlcipher/example/windows/runner/resource.h b/packages/powersync_sqlcipher/example/windows/runner/resource.h new file mode 100644 index 00000000..66a65d1e --- /dev/null +++ b/packages/powersync_sqlcipher/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/packages/powersync_sqlcipher/example/windows/runner/resources/app_icon.ico b/packages/powersync_sqlcipher/example/windows/runner/resources/app_icon.ico new file mode 100644 index 00000000..c04e20ca Binary files /dev/null and b/packages/powersync_sqlcipher/example/windows/runner/resources/app_icon.ico differ diff --git a/packages/powersync_sqlcipher/example/windows/runner/runner.exe.manifest b/packages/powersync_sqlcipher/example/windows/runner/runner.exe.manifest new file mode 100644 index 00000000..153653e8 --- /dev/null +++ b/packages/powersync_sqlcipher/example/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/packages/powersync_sqlcipher/example/windows/runner/utils.cpp b/packages/powersync_sqlcipher/example/windows/runner/utils.cpp new file mode 100644 index 00000000..3a0b4651 --- /dev/null +++ b/packages/powersync_sqlcipher/example/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/packages/powersync_sqlcipher/example/windows/runner/utils.h b/packages/powersync_sqlcipher/example/windows/runner/utils.h new file mode 100644 index 00000000..3879d547 --- /dev/null +++ b/packages/powersync_sqlcipher/example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/powersync_sqlcipher/example/windows/runner/win32_window.cpp b/packages/powersync_sqlcipher/example/windows/runner/win32_window.cpp new file mode 100644 index 00000000..60608d0f --- /dev/null +++ b/packages/powersync_sqlcipher/example/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/packages/powersync_sqlcipher/example/windows/runner/win32_window.h b/packages/powersync_sqlcipher/example/windows/runner/win32_window.h new file mode 100644 index 00000000..e901dde6 --- /dev/null +++ b/packages/powersync_sqlcipher/example/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/packages/powersync_sqlcipher/lib/powersync.dart b/packages/powersync_sqlcipher/lib/powersync.dart new file mode 100644 index 00000000..4b96c605 --- /dev/null +++ b/packages/powersync_sqlcipher/lib/powersync.dart @@ -0,0 +1,10 @@ +/// PowerSync with Encryption for Flutter. +/// +/// Use [PowerSyncSQLCipherOpenFactory] to open an encrypted database. +library; + +export 'package:powersync_core/powersync_core.dart'; + +export 'src/stub.dart' + if (dart.library.js_interop) 'src/web_encryption.dart' + if (dart.library.ffi) 'src/sqlcipher.dart'; diff --git a/packages/powersync_sqlcipher/lib/sqlite3.dart b/packages/powersync_sqlcipher/lib/sqlite3.dart new file mode 100644 index 00000000..2941247e --- /dev/null +++ b/packages/powersync_sqlcipher/lib/sqlite3.dart @@ -0,0 +1,5 @@ +/// Re-exports [sqlite3](https://pub.dev/packages/sqlite3) to expose sqlite3 without +/// adding it as a direct dependency. +library; + +export 'package:powersync_core/sqlite3.dart'; diff --git a/packages/powersync_sqlcipher/lib/sqlite3_common.dart b/packages/powersync_sqlcipher/lib/sqlite3_common.dart new file mode 100644 index 00000000..52887937 --- /dev/null +++ b/packages/powersync_sqlcipher/lib/sqlite3_common.dart @@ -0,0 +1,5 @@ +/// Re-exports [sqlite3_common](https://pub.dev/packages/sqlite3) to expose sqlite3_common without +/// adding it as a direct dependency. +library; + +export 'package:powersync_core/sqlite3_common.dart'; diff --git a/packages/powersync_sqlcipher/lib/sqlite3_open.dart b/packages/powersync_sqlcipher/lib/sqlite3_open.dart new file mode 100644 index 00000000..fd364dc4 --- /dev/null +++ b/packages/powersync_sqlcipher/lib/sqlite3_open.dart @@ -0,0 +1,5 @@ +/// Re-exports [sqlite3_open](https://pub.dev/packages/sqlite3) to expose sqlite3_open without +/// adding it as a direct dependency. +library; + +export 'package:powersync_core/sqlite3_open.dart'; diff --git a/packages/powersync_sqlcipher/lib/sqlite_async.dart b/packages/powersync_sqlcipher/lib/sqlite_async.dart new file mode 100644 index 00000000..6deed835 --- /dev/null +++ b/packages/powersync_sqlcipher/lib/sqlite_async.dart @@ -0,0 +1,5 @@ +/// Re-exports [sqlite_async](https://pub.dev/packages/sqlite_async) to expose sqlite_async without +/// adding it as a direct dependency. +library; + +export 'package:powersync_core/sqlite_async.dart'; diff --git a/packages/powersync_sqlcipher/lib/src/shared.dart b/packages/powersync_sqlcipher/lib/src/shared.dart new file mode 100644 index 00000000..e1d4046f --- /dev/null +++ b/packages/powersync_sqlcipher/lib/src/shared.dart @@ -0,0 +1,21 @@ +import '../sqlite_async.dart'; + +const defaultOptions = SqliteOptions( + webSqliteOptions: WebSqliteOptions( + wasmUri: 'sqlite3mc.wasm', workerUri: 'powersync_db.worker.js'), +); + +mixin BaseSQLCipherFactoryMixin on AbstractDefaultSqliteOpenFactory { + String get key; + + @override + List pragmaStatements(SqliteOpenOptions options) { + final basePragmaStatements = super.pragmaStatements(options); + return [ + // Set the encryption key as the first statement + "PRAGMA KEY = ${quoteString(key)}", + // Include the default statements afterwards + for (var statement in basePragmaStatements) statement + ]; + } +} diff --git a/packages/powersync_sqlcipher/lib/src/sqlcipher.dart b/packages/powersync_sqlcipher/lib/src/sqlcipher.dart new file mode 100644 index 00000000..2866cd2d --- /dev/null +++ b/packages/powersync_sqlcipher/lib/src/sqlcipher.dart @@ -0,0 +1,36 @@ +import 'package:powersync_core/powersync_core.dart'; +import 'package:powersync_core/sqlite3_common.dart'; +import 'package:powersync_core/sqlite3_open.dart' as sqlite3_open; +import 'package:powersync_core/sqlite_async.dart'; +import 'package:sqlcipher_flutter_libs/sqlcipher_flutter_libs.dart'; + +import 'shared.dart'; + +class _NativeCipherOpenFactory extends PowerSyncOpenFactory + with BaseSQLCipherFactoryMixin { + @override + final String key; + + _NativeCipherOpenFactory({ + required super.path, + required this.key, + // ignore: unused_element_parameter + super.sqliteOptions = defaultOptions, + }); + + @override + CommonDatabase open(SqliteOpenOptions options) { + sqlite3_open.open + .overrideFor(sqlite3_open.OperatingSystem.android, openCipherOnAndroid); + + var db = super.open(options); + final versionRows = db.select('PRAGMA cipher_version'); + if (versionRows.isEmpty) { + throw StateError( + "SQLCipher was not initialized correctly. 'PRAGMA cipher_version' returned no rows."); + } + return db; + } +} + +typedef PowerSyncSQLCipherOpenFactory = _NativeCipherOpenFactory; diff --git a/packages/powersync_sqlcipher/lib/src/stub.dart b/packages/powersync_sqlcipher/lib/src/stub.dart new file mode 100644 index 00000000..543552a9 --- /dev/null +++ b/packages/powersync_sqlcipher/lib/src/stub.dart @@ -0,0 +1,16 @@ +import '../powersync.dart'; +import 'shared.dart'; + +/// A factory for opening a database with SQLCipher encryption. +/// An encryption [key] is required to open the database. +class PowerSyncSQLCipherOpenFactory extends PowerSyncOpenFactory { + PowerSyncSQLCipherOpenFactory({ + required super.path, + required this.key, + super.sqliteOptions = defaultOptions, + }) { + throw UnsupportedError('Unsupported platform for powersync_sqlcipher'); + } + + final String key; +} diff --git a/packages/powersync_sqlcipher/lib/src/web_encryption.dart b/packages/powersync_sqlcipher/lib/src/web_encryption.dart new file mode 100644 index 00000000..882442ae --- /dev/null +++ b/packages/powersync_sqlcipher/lib/src/web_encryption.dart @@ -0,0 +1,29 @@ +import 'package:powersync_core/web.dart'; + +import 'shared.dart'; + +class _WebEncryptionFactory extends PowerSyncWebOpenFactory + with BaseSQLCipherFactoryMixin { + @override + final String key; + + _WebEncryptionFactory({ + required super.path, + required this.key, + // ignore: unused_element_parameter + super.sqliteOptions = defaultOptions, + }); + + @override + Future connectToWorker( + WebSqlite sqlite, String name) async { + return sqlite.connectToRecommended( + name, + additionalOptions: PowerSyncAdditionalOpenOptions( + useMultipleCiphersVfs: true, + ), + ); + } +} + +typedef PowerSyncSQLCipherOpenFactory = _WebEncryptionFactory; diff --git a/packages/powersync_sqlcipher/pubspec.yaml b/packages/powersync_sqlcipher/pubspec.yaml new file mode 100644 index 00000000..b73913c2 --- /dev/null +++ b/packages/powersync_sqlcipher/pubspec.yaml @@ -0,0 +1,33 @@ +name: powersync_sqlcipher +version: 0.1.13 +homepage: https://powersync.com +repository: https://github.com/powersync-ja/powersync.dart +description: PowerSync Flutter SDK - sync engine for building local-first apps. + +environment: + sdk: ">=3.4.3 <4.0.0" + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + + powersync_core: ^1.6.1 + powersync_flutter_libs: ^0.4.12 + sqlcipher_flutter_libs: ^0.6.4 + sqlite3_web: ^0.3.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + test_api: ^0.7.0 + path: ^1.8.3 + path_provider: ^2.0.13 + +platforms: + android: + ios: + linux: + macos: + windows: diff --git a/packages/sqlite3_wasm_build/.gitignore b/packages/sqlite3_wasm_build/.gitignore new file mode 100644 index 00000000..4c6fda5b --- /dev/null +++ b/packages/sqlite3_wasm_build/.gitignore @@ -0,0 +1,4 @@ +node_modules +pnpm-lock.yaml +sqlite3.dart +dist diff --git a/packages/sqlite3_wasm_build/.npmignore b/packages/sqlite3_wasm_build/.npmignore new file mode 100644 index 00000000..76071ace --- /dev/null +++ b/packages/sqlite3_wasm_build/.npmignore @@ -0,0 +1 @@ +sqlite3.dart/ diff --git a/packages/sqlite3_wasm_build/README.md b/packages/sqlite3_wasm_build/README.md new file mode 100644 index 00000000..7d787089 --- /dev/null +++ b/packages/sqlite3_wasm_build/README.md @@ -0,0 +1,18 @@ +This directory includes tools to build `sqlite3.wasm` files compatible with the +PowerSync Dart SDK. + +This build process is adapted from [`package:sqlite3`][upstream], with patches +applied to link the [PowerSync SQLite extension][core] statically. + +### Working on patches + +To adapt the patches from an `$old` version to a `$new` version: + +1. Clone `https://github.com/simolus3/sqlite3.dart.git` somewhere. +2. Create a branch tracking the old version: `git switch -c powersync-patches sqlite3-$old`. +3. Apply the existing patches: `git am patches/*`. +4. Rebase onto a new upstream release if necessary: `git rebase --onto sqlite3-$new sqlite3-$old powersync-patches` +5. Obtain a new patchset with `git format-patch sqlite3-$new..HEAD`. + +[upstream]: https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3/assets/wasm +[core]: https://github.com/powersync-ja/powersync-sqlite-core diff --git a/packages/sqlite3_wasm_build/build.sh b/packages/sqlite3_wasm_build/build.sh new file mode 100755 index 00000000..eefbb4ae --- /dev/null +++ b/packages/sqlite3_wasm_build/build.sh @@ -0,0 +1,28 @@ +#!/bin/sh +set -e + +SQLITE_VERSION="2.9.0" +POWERSYNC_CORE_VERSION="0.4.6" +SQLITE_PATH="sqlite3.dart" + +if [ -d "$SQLITE_PATH" ]; then + echo "Deleting existing clone" + rm -rf $SQLITE_PATH +fi + +git clone --branch "sqlite3-$SQLITE_VERSION" --depth 1 https://github.com/simolus3/sqlite3.dart.git $SQLITE_PATH + +cd $SQLITE_PATH +git apply ../patches/* + +cd "sqlite3/" + +cmake -Dwasi_sysroot=/opt/homebrew/share/wasi-sysroot \ + -Dclang=/opt/homebrew/opt/llvm/bin/clang\ + -DPOWERSYNC_VERSION="$POWERSYNC_CORE_VERSION" \ + -S assets/wasm -B .dart_tool/sqlite3_build +cmake --build .dart_tool/sqlite3_build/ -t output -j + +cd ../../ +mkdir -p dist +cp $SQLITE_PATH/sqlite3/example/web/*.wasm dist diff --git a/packages/sqlite3_wasm_build/package.json b/packages/sqlite3_wasm_build/package.json new file mode 100644 index 00000000..ad77977a --- /dev/null +++ b/packages/sqlite3_wasm_build/package.json @@ -0,0 +1,10 @@ +{ + "name": "@powersync/dart-wasm-bundles", + "version": "0.0.1", + "description": "WebAssembly modules containing SQLite + PowerSync for Dart apps.", + "repository": { + "type": "git", + "url": "git+https://github.com/powersync-ja/powersync.dart.git", + "directory": "packages/sqlite3_wasm_build" + } +} diff --git a/packages/sqlite3_wasm_build/patches/0001-Link-PowerSync-core-extension.patch b/packages/sqlite3_wasm_build/patches/0001-Link-PowerSync-core-extension.patch new file mode 100644 index 00000000..44d6e139 --- /dev/null +++ b/packages/sqlite3_wasm_build/patches/0001-Link-PowerSync-core-extension.patch @@ -0,0 +1,82 @@ +From 80ec8f3a54d938c6bf39b471aa26c4c77f9984a2 Mon Sep 17 00:00:00 2001 +From: Simon Binder +Date: Mon, 27 Jan 2025 15:12:53 +0100 +Subject: [PATCH] Link PowerSync core extension + +--- + sqlite3/assets/wasm/CMakeLists.txt | 7 ++++++- + sqlite3/assets/wasm/os_web.c | 11 ++++++++++- + sqlite3/assets/wasm/sqlite_cfg.h | 2 +- + 3 files changed, 17 insertions(+), 3 deletions(-) + +diff --git a/sqlite3/assets/wasm/CMakeLists.txt b/sqlite3/assets/wasm/CMakeLists.txt +index 6597d86..81a5a41 100644 +--- a/sqlite3/assets/wasm/CMakeLists.txt ++++ b/sqlite3/assets/wasm/CMakeLists.txt +@@ -6,6 +6,7 @@ project(${PROJECT_NAME} LANGUAGES C) + set(triple wasm32-unknown-wasi) + set(wasi_sysroot "/usr/share/wasi-sysroot" CACHE PATH "Path to wasi sysroot") + set(clang "clang" CACHE FILEPATH "Path to wasm-capable clang executable") ++set(POWERSYNC_VERSION "0.3.9" CACHE STRING "PowerSync core version to use") + + include(FetchContent) + +@@ -27,6 +28,9 @@ FetchContent_MakeAvailable(sqlite3mc) + + file(DOWNLOAD https://raw.githubusercontent.com/sqlite/sqlite/master/src/test_vfstrace.c "${CMAKE_BINARY_DIR}/vfstrace.c") + ++set(POWERSYNC_A "${CMAKE_BINARY_DIR}/libpowersync-wasm.a") ++file(DOWNLOAD "https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v${POWERSYNC_VERSION}/libpowersync-wasm.a" "${POWERSYNC_A}") ++ + # Generate symbols we need to export from the sqlite3.wasm build + add_custom_command( + OUTPUT required_symbols.txt +@@ -88,8 +92,9 @@ macro(base_sqlite3_target name debug crypto) + -Wl,--import-memory + --sysroot ${wasi_sysroot} + ${sources} ++ ${POWERSYNC_A} + @${CMAKE_CURRENT_BINARY_DIR}/required_symbols.txt +- DEPENDS ${sources} required_symbols ++ DEPENDS ${sources} ${POWERSYNC_A} required_symbols + VERBATIM + ) + +diff --git a/sqlite3/assets/wasm/os_web.c b/sqlite3/assets/wasm/os_web.c +index 4a1df1d..be9c3c9 100644 +--- a/sqlite3/assets/wasm/os_web.c ++++ b/sqlite3/assets/wasm/os_web.c +@@ -6,7 +6,16 @@ + #include "bridge.h" + #include "sqlite3.h" + +-int sqlite3_os_init(void) { return SQLITE_OK; } ++extern int sqlite3_powersync_init(sqlite3 *db, char **pzErrMsg, ++ const sqlite3_api_routines *pApi); ++ ++int sqlite3_os_init(void) { ++ int rc = sqlite3_auto_extension((void (*)(void)) & sqlite3_powersync_init); ++ if (rc != SQLITE_OK) { ++ return rc; ++ } ++ return SQLITE_OK; ++} + + int sqlite3_os_end(void) { return SQLITE_OK; } + +diff --git a/sqlite3/assets/wasm/sqlite_cfg.h b/sqlite3/assets/wasm/sqlite_cfg.h +index 74e8dd7..682acbf 100644 +--- a/sqlite3/assets/wasm/sqlite_cfg.h ++++ b/sqlite3/assets/wasm/sqlite_cfg.h +@@ -42,7 +42,7 @@ + // Disable things we don't need + #define SQLITE_OMIT_DEPRECATED + #define SQLITE_OMIT_PROGRESS_CALLBACK +-#define SQLITE_OMIT_AUTHORIZATION ++// #define SQLITE_OMIT_AUTHORIZATION // This breaks the powersync-core build + #define SQLITE_UNTESTABLE + #define SQLITE_OMIT_COMPILEOPTION_DIAGS + #define SQLITE_OMIT_LOAD_EXTENSION +-- +2.39.5 (Apple Git-154) + diff --git a/scripts/compile_webworker.dart b/scripts/compile_webworker.dart index 3e85f8a9..c40f7746 100644 --- a/scripts/compile_webworker.dart +++ b/scripts/compile_webworker.dart @@ -12,8 +12,8 @@ Future main() async { final dbWorkerOutputPath = path.join(repoRoot, 'packages/powersync/assets/$workerFilename'); - final workerSourcePath = path.join( - repoRoot, './packages/powersync/lib/src/web/powersync_db.worker.dart'); + final workerSourcePath = path.join(repoRoot, + './packages/powersync_core/lib/src/web/powersync_db.worker.dart'); // And compile worker code final dbWorkerProcess = await Process.run( @@ -37,8 +37,8 @@ Future main() async { final syncWorkerOutputPath = path.join(repoRoot, 'packages/powersync/assets/$syncWorkerFilename'); - final syncWorkerSourcePath = - path.join(repoRoot, './packages/powersync/lib/src/web/sync_worker.dart'); + final syncWorkerSourcePath = path.join( + repoRoot, './packages/powersync_core/lib/src/web/sync_worker.dart'); final syncWorkerProcess = await Process.run( Platform.executable, @@ -57,6 +57,15 @@ Future main() async { 'Could not compile sync worker:\nstdout: ${syncWorkerProcess.stdout.toString()}\nstderr: ${syncWorkerProcess.stderr.toString()}'); } + final workerFile = File(dbWorkerOutputPath); + final syncWorkerFile = File(syncWorkerOutputPath); + + //Copy workers to powersync_core + final powersyncCoreAssetsPath = + path.join(repoRoot, 'packages/powersync_core/assets'); + workerFile.copySync('$powersyncCoreAssetsPath/$workerFilename'); + syncWorkerFile.copySync('$powersyncCoreAssetsPath/$syncWorkerFilename'); + // Copy this to all demo apps web folders final demosRoot = path.join(repoRoot, 'demos'); final demoDirectories = diff --git a/scripts/download_core_binary_demos.dart b/scripts/download_core_binary_demos.dart index 96b15e37..f5453ce1 100644 --- a/scripts/download_core_binary_demos.dart +++ b/scripts/download_core_binary_demos.dart @@ -3,7 +3,7 @@ import 'dart:io'; final coreUrl = - 'https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v0.3.6'; + 'https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v0.4.6'; void main() async { final powersyncLibsLinuxPath = "packages/powersync_flutter_libs/linux"; diff --git a/scripts/init_powersync_core_binary.dart b/scripts/init_powersync_core_binary.dart index 05b06380..0c985895 100644 --- a/scripts/init_powersync_core_binary.dart +++ b/scripts/init_powersync_core_binary.dart @@ -1,16 +1,16 @@ -/// Downloads the powersync dynamic library and copies it to the powersync package directory -/// This is only necessary for running unit tests in the powersync package +/// Downloads the powersync dynamic library and copies it to the powersync_core package directory +/// This is only necessary for running unit tests in the powersync_core package import 'dart:ffi'; import 'dart:io'; import 'package:melos/melos.dart'; final sqliteUrl = - 'https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v0.3.6'; + 'https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v0.4.6'; void main() async { final sqliteCoreFilename = getLibraryForPlatform(); - final powersyncPath = "packages/powersync"; + final powersyncPath = "packages/powersync_core"; final sqliteCorePath = '$powersyncPath/$sqliteCoreFilename'; // Download dynamic library diff --git a/tool/update_version.dart b/tool/update_version.dart index 7d7e8ee7..a6db98b0 100644 --- a/tool/update_version.dart +++ b/tool/update_version.dart @@ -2,11 +2,11 @@ import 'dart:io'; import 'package:yaml/yaml.dart'; void main() { - final pubspecFile = File('packages/powersync/pubspec.yaml'); + final pubspecFile = File('packages/powersync_core/pubspec.yaml'); final pubspecContent = pubspecFile.readAsStringSync(); final yaml = loadYaml(pubspecContent); final version = yaml['version']; - final versionFile = File('packages/powersync/lib/src/version.dart'); + final versionFile = File('packages/powersync_core/lib/src/version.dart'); versionFile.writeAsStringSync("const String libraryVersion = '$version';\n"); }