diff --git a/.github/workflows/demos.yml b/.github/workflows/demos.yml index 78023fc3..0ae30733 100644 --- a/.github/workflows/demos.yml +++ b/.github/workflows/demos.yml @@ -7,7 +7,7 @@ concurrency: on: push: branches: - - '**' + - "**" jobs: build: @@ -17,8 +17,8 @@ jobs: - name: Install Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.x' - channel: 'stable' + flutter-version: "3.x" + channel: "stable" - name: Install Melos run: flutter pub global activate melos @@ -38,11 +38,13 @@ jobs: - name: Install Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.x' - channel: 'stable' + flutter-version: "3.x" + channel: "stable" - name: Install melos run: flutter pub global activate melos - name: Install dependencies run: melos prepare - - name: Run tests + - name: Run flutter tests run: melos test + - name: Run dart tests + run: melos test:web diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index e7323a2b..88acf4bd 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -7,7 +7,7 @@ concurrency: on: push: branches: - - '**' + - "**" jobs: build: @@ -17,8 +17,8 @@ jobs: - name: Install Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.x' - channel: 'stable' + flutter-version: "3.x" + channel: "stable" - name: Install Melos run: flutter pub global activate melos @@ -42,12 +42,13 @@ jobs: - name: Install Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.x' - channel: 'stable' + 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 tests + - name: Run flutter tests run: melos test + - name: Run dart tests + run: melos test:web diff --git a/.gitignore b/.gitignore index 1f9cd70b..7a899ee7 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,8 @@ assets # Web assets powersync_db.worker.js sqlite3.wasm + +#Core binaries +*.dylib +*.dll +*.so diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f1d1ec8..cdafab05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,19 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## 2024-07-16 + +### Changes + +--- + +- [`powersync` - `v1.5.5`](#powersync---v155) +- [`powersync_attachments_helper` - `v0.5.1+1`](#powersync_attachments_helper---v0511) + +#### `powersync` - `v1.5.5` + +- Fix issue where `hasSynced` is cleared when offline. + ## 2024-07-10 ### Changes @@ -11,25 +24,24 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline Packages with breaking changes: - - There are no breaking changes in this release. +- There are no breaking changes in this release. Packages with other changes: - - [`powersync` - `v1.3.0-alpha.9`](#powersync---v130-alpha9) - - [`powersync_attachments_helper` - `v0.3.0-alpha.4`](#powersync_attachments_helper---v030-alpha4) +- [`powersync` - `v1.3.0-alpha.9`](#powersync---v130-alpha9) +- [`powersync_attachments_helper` - `v0.3.0-alpha.4`](#powersync_attachments_helper---v030-alpha4) 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.3.0-alpha.4` +- `powersync_attachments_helper` - `v0.3.0-alpha.4` --- #### `powersync` - `v1.3.0-alpha.9` - - Updated sqlite_async to use Navigator locks for limiting sync stream implementions in multiple tabs - +- Updated sqlite_async to use Navigator locks for limiting sync stream implementions in multiple tabs ## 2024-07-04 @@ -39,26 +51,25 @@ Packages with dependency updates only: Packages with breaking changes: - - There are no breaking changes in this release. +- There are no breaking changes in this release. Packages with other changes: - - [`powersync` - `v1.3.0-alpha.8`](#powersync---v130-alpha8) - - [`powersync_attachments_helper` - `v0.3.0-alpha.3`](#powersync_attachments_helper---v030-alpha3) +- [`powersync` - `v1.3.0-alpha.8`](#powersync---v130-alpha8) +- [`powersync_attachments_helper` - `v0.3.0-alpha.3`](#powersync_attachments_helper---v030-alpha3) 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.3.0-alpha.3` +- `powersync_attachments_helper` - `v0.3.0-alpha.3` --- #### `powersync` - `v1.3.0-alpha.8` - - **FIX**(powersync-attachements-helper): pubspec file (#29). - - **DOCS**: update readme and getting started (#51). - +- **FIX**(powersync-attachements-helper): pubspec file (#29). +- **DOCS**: update readme and getting started (#51). ## 2024-05-30 @@ -68,28 +79,27 @@ Packages with dependency updates only: Packages with breaking changes: - - [`powersync_attachments_helper` - `v0.3.0-alpha.2`](#powersync_attachments_helper---v030-alpha2) +- [`powersync_attachments_helper` - `v0.3.0-alpha.2`](#powersync_attachments_helper---v030-alpha2) Packages with other changes: - - [`powersync` - `v1.3.0-alpha.5`](#powersync---v130-alpha5) +- [`powersync` - `v1.3.0-alpha.5`](#powersync---v130-alpha5) --- #### `powersync_attachments_helper` - `v0.3.0-alpha.2` - - **FIX**: reset isProcessing when exception is thrown during sync process. (#81). - - **FIX**: attachment queue duplicating requests (#68). - - **FIX**(powersync-attachements-helper): pubspec file (#29). - - **FEAT**(attachments): add error handlers (#65). - - **DOCS**: update readmes (#38). - - **BREAKING** **FEAT**(attachments): cater for subdirectories in storage (#78). +- **FIX**: reset isProcessing when exception is thrown during sync process. (#81). +- **FIX**: attachment queue duplicating requests (#68). +- **FIX**(powersync-attachements-helper): pubspec file (#29). +- **FEAT**(attachments): add error handlers (#65). +- **DOCS**: update readmes (#38). +- **BREAKING** **FEAT**(attachments): cater for subdirectories in storage (#78). #### `powersync` - `v1.3.0-alpha.5` - - **FIX**(powersync-attachements-helper): pubspec file (#29). - - **DOCS**: update readme and getting started (#51). - +- **FIX**(powersync-attachements-helper): pubspec file (#29). +- **DOCS**: update readme and getting started (#51). ## 2024-03-05 @@ -99,25 +109,24 @@ Packages with other changes: Packages with breaking changes: - - There are no breaking changes in this release. +- There are no breaking changes in this release. Packages with other changes: - - [`powersync` - `v1.3.0-alpha.3`](#powersync---v130-alpha3) - - [`powersync_attachments_helper` - `v0.3.0-alpha.2`](#powersync_attachments_helper---v030-alpha2) +- [`powersync` - `v1.3.0-alpha.3`](#powersync---v130-alpha3) +- [`powersync_attachments_helper` - `v0.3.0-alpha.2`](#powersync_attachments_helper---v030-alpha2) 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.3.0-alpha.2` +- `powersync_attachments_helper` - `v0.3.0-alpha.2` --- #### `powersync` - `v1.3.0-alpha.3` - - Fixed issue where disconnectAndClear would prevent subsequent sync connection on native platforms and would fail to clear the database on web. - +- Fixed issue where disconnectAndClear would prevent subsequent sync connection on native platforms and would fail to clear the database on web. ## 2024-02-15 @@ -127,23 +136,23 @@ Packages with dependency updates only: Packages with breaking changes: - - There are no breaking changes in this release. +- There are no breaking changes in this release. Packages with other changes: - - [`powersync` - `v1.3.0-alpha.2`](#powersync---v130-alpha2) - - [`powersync_attachments_helper` - `v0.3.0-alpha.2`](#powersync_attachments_helper---v030-alpha2) +- [`powersync` - `v1.3.0-alpha.2`](#powersync---v130-alpha2) +- [`powersync_attachments_helper` - `v0.3.0-alpha.2`](#powersync_attachments_helper---v030-alpha2) 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.3.0-alpha.2` +- `powersync_attachments_helper` - `v0.3.0-alpha.2` --- #### `powersync` - `v1.3.0-alpha.2` - - **FIX**(powersync-attachements-helper): pubspec file (#29). - - **DOCS**: update readme and getting started (#51). - +- **FIX**(powersync-attachements-helper): pubspec file (#29). +- **DOCS**: update readme and getting started (#51). +- `powersync_attachments_helper` - `v0.5.1+1` diff --git a/README.md b/README.md index bea070c0..6577d87f 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,15 @@

-*Bad connectivity is everywhere, and we're tired of it. PowerSync is on a mission to help developers write offline-first real-time reactive apps.* +*[PowerSync](https://www.powersync.com) is a Postgres-SQLite sync layer, which helps developers to create local-first real-time reactive apps that work seamlessly both online and offline.* PowerSync SDK for Dart and Flutter =========== | package | build | pub | likes | popularity | pub points | |----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------| ------- | ------- | -| 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 | [![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) +| [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) #### Usage @@ -27,6 +27,6 @@ To configure the monorepo for development run `melos prepare` after cloning #### Resources - [![PowerSync docs](https://img.shields.io/badge/documentation-powersync.com-green.svg?label=flutter%20docs)](https://docs.powersync.com/client-sdk-references/flutter) -- [![Discord Chat](https://img.shields.io/discord/1138230179878154300?style=social&logo=discord&logoColor=%235865f2&label=Join%20Discord%20server)](https://discord.gg/powersync) -- [![Twitter Follow](https://img.shields.io/twitter/follow/powersync?label=PowerSync&style=social)](https://twitter.com/intent/follow?screen_name=powersync_) +- [![Discord](https://img.shields.io/discord/1138230179878154300?style=social&logo=discord&logoColor=%235865f2&label=Join%20Discord%20server)](https://discord.gg/powersync) +- [![Twitter follow](https://img.shields.io/twitter/follow/powersync?label=PowerSync&style=social)](https://twitter.com/intent/follow?screen_name=powersync_) - [![YouTube](https://img.shields.io/youtube/channel/subscribers/UCSDdZvrZuizmc2EMBuTs2Qg?style=social&label=YouTube%20%40powersync_)](https://twitter.com/intent/follow?screen_name=powersync_) diff --git a/demos/django-todolist/.gitignore b/demos/django-todolist/.gitignore new file mode 100644 index 00000000..0b04140a --- /dev/null +++ b/demos/django-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/django-todolist/LICENSE b/demos/django-todolist/LICENSE new file mode 100644 index 00000000..0e259d42 --- /dev/null +++ b/demos/django-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/django-todolist/README.md b/demos/django-todolist/README.md new file mode 100644 index 00000000..a7b8c3d0 --- /dev/null +++ b/demos/django-todolist/README.md @@ -0,0 +1,71 @@ +# PowerSync + Django Flutter Demo: Todo List App + +Demo app demonstrating use of the PowerSync SDK for Flutter together with the [demo Django backend](https://github.com/powersync-ja/powersync-django-backend-todolist-demo). + +# Running the app + +Ensure you have [melos](https://melos.invertase.dev/~melos-latest/getting-started) installed. + +1. `cd demos/django-todolist` +2. `melos bootstrap` +3. `cp lib/app_config_template.dart lib/app_config.dart` +4. Insert your Django URL and PowerSync project credentials into `lib/app_config.dart` (See instructions below) +5. `flutter run` + +A test user with the following credentials will be available: + +``` +username: testuser +password: testpassword +``` + +# Service Configuration + +This demo can be used with cloud or local services. + +## Local Services + +The [Self Hosting Demo](https://github.com/powersync-ja/self-host-demo) repository contains a Docker Compose Django backend demo which can be used with this client. +See [instructions](https://github.com/powersync-ja/self-host-demo/blob/main/demos/django/README.md) for starting the backend locally. + +The backend demo should perform all the required setup automatically. + +### Android + +Note that Android requires port forwarding of local services. These can be configured with ADB as below: + +```bash +adb reverse tcp:8080 tcp:8080 && adb reverse tcp:6061 tcp:6061 +``` + +## Cloud Services + +### Set up Django project + +Follow the instructions in the django backend project's README. + +The instructions guide you through the following: + +1. Creates `lists` and `todos` tables. +2. Creates a test user. +3. Create a logical replication publication called `powersync` for `lists` and `todos`. + +### Set up PowerSync Instance + +Create a new PowerSync instance by signing up for PowerSync Cloud at www.powersync.com, then connect to the database of your Django 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 = token_parameters.user_id + data: + - select * from lists where id = bucket.list_id + - select * from todos where list_id = bucket.list_id +``` + +### Configure the app + +Insert the credentials of your new Django backend and PowerSync projects into `lib/app_config.dart` diff --git a/demos/django-todolist/analysis_options.yaml b/demos/django-todolist/analysis_options.yaml new file mode 100644 index 00000000..61b6c4de --- /dev/null +++ b/demos/django-todolist/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/django-todolist/android/.gitignore b/demos/django-todolist/android/.gitignore new file mode 100644 index 00000000..6f568019 --- /dev/null +++ b/demos/django-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/django-todolist/android/app/build.gradle b/demos/django-todolist/android/app/build.gradle new file mode 100644 index 00000000..9daa778b --- /dev/null +++ b/demos/django-todolist/android/app/build.gradle @@ -0,0 +1,70 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + 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 { + 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 + 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 '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/demos/django-todolist/android/app/src/debug/AndroidManifest.xml b/demos/django-todolist/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..f19dd7d6 --- /dev/null +++ b/demos/django-todolist/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + diff --git a/demos/django-todolist/android/app/src/main/AndroidManifest.xml b/demos/django-todolist/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..0210e54e --- /dev/null +++ b/demos/django-todolist/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/demos/django-todolist/android/app/src/main/kotlin/co/powersync/demotodolist/MainActivity.kt b/demos/django-todolist/android/app/src/main/kotlin/co/powersync/demotodolist/MainActivity.kt new file mode 100644 index 00000000..88fda765 --- /dev/null +++ b/demos/django-todolist/android/app/src/main/kotlin/co/powersync/demotodolist/MainActivity.kt @@ -0,0 +1,6 @@ +package co.powersync.demotodolist + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/demos/django-todolist/android/app/src/main/res/drawable-v21/launch_background.xml b/demos/django-todolist/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/demos/django-todolist/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/demos/django-todolist/android/app/src/main/res/drawable/launch_background.xml b/demos/django-todolist/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/demos/django-todolist/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/demos/django-todolist/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/django-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/django-todolist/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/demos/django-todolist/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/django-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/django-todolist/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/demos/django-todolist/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/django-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/django-todolist/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/demos/django-todolist/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/django-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/django-todolist/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/demos/django-todolist/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/django-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/django-todolist/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/demos/django-todolist/android/app/src/main/res/values-night/styles.xml b/demos/django-todolist/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..06952be7 --- /dev/null +++ b/demos/django-todolist/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/demos/django-todolist/android/app/src/main/res/values/styles.xml b/demos/django-todolist/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..cb1ef880 --- /dev/null +++ b/demos/django-todolist/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/demos/django-todolist/android/app/src/profile/AndroidManifest.xml b/demos/django-todolist/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..f19dd7d6 --- /dev/null +++ b/demos/django-todolist/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + diff --git a/demos/django-todolist/android/build.gradle b/demos/django-todolist/android/build.gradle new file mode 100644 index 00000000..713d7f6e --- /dev/null +++ b/demos/django-todolist/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.2.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +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/django-todolist/android/gradle.properties b/demos/django-todolist/android/gradle.properties new file mode 100644 index 00000000..94adc3a3 --- /dev/null +++ b/demos/django-todolist/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/demos/django-todolist/android/gradle/wrapper/gradle-wrapper.properties b/demos/django-todolist/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..3c472b99 --- /dev/null +++ b/demos/django-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.5-all.zip diff --git a/demos/django-todolist/android/settings.gradle b/demos/django-todolist/android/settings.gradle new file mode 100644 index 00000000..44e62bcf --- /dev/null +++ b/demos/django-todolist/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/demos/django-todolist/ios/.gitignore b/demos/django-todolist/ios/.gitignore new file mode 100644 index 00000000..7a7f9873 --- /dev/null +++ b/demos/django-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/django-todolist/ios/Flutter/AppFrameworkInfo.plist b/demos/django-todolist/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 00000000..7c569640 --- /dev/null +++ b/demos/django-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/django-todolist/ios/Flutter/Debug.xcconfig b/demos/django-todolist/ios/Flutter/Debug.xcconfig new file mode 100644 index 00000000..ec97fc6f --- /dev/null +++ b/demos/django-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/django-todolist/ios/Flutter/Release.xcconfig b/demos/django-todolist/ios/Flutter/Release.xcconfig new file mode 100644 index 00000000..c4855bfe --- /dev/null +++ b/demos/django-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/django-todolist/ios/Podfile b/demos/django-todolist/ios/Podfile new file mode 100644 index 00000000..e9f73048 --- /dev/null +++ b/demos/django-todolist/ios/Podfile @@ -0,0 +1,48 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '12.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/django-todolist/ios/Podfile.lock b/demos/django-todolist/ios/Podfile.lock new file mode 100644 index 00000000..2e9ceff2 --- /dev/null +++ b/demos/django-todolist/ios/Podfile.lock @@ -0,0 +1,64 @@ +PODS: + - Flutter (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - powersync-sqlite-core (0.1.6) + - powersync_flutter_libs (0.0.1): + - Flutter + - powersync-sqlite-core (~> 0.1.6) + - 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/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) + - sqlite3/fts5 + - 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`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) + +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" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + sqlite3_flutter_libs: + :path: ".symlinks/plugins/sqlite3_flutter_libs/ios" + +SPEC CHECKSUMS: + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + powersync-sqlite-core: 4c38c8f470f6dca61346789fd5436a6826d1e3dd + powersync_flutter_libs: 5d6b132a398de442c0853a8b14bfbb62cd4ff5a1 + shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 + sqlite3: 292c3e1bfe89f64e51ea7fc7dab9182a017c8630 + sqlite3_flutter_libs: 0d611efdf6d1c9297d5ab03dab21b75aeebdae31 + +PODFILE CHECKSUM: f7b3cb7384a2d5da4b22b090e1f632de7f377987 + +COCOAPODS: 1.15.2 diff --git a/demos/django-todolist/ios/Runner.xcodeproj/project.pbxproj b/demos/django-todolist/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..16636b7a --- /dev/null +++ b/demos/django-todolist/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,552 @@ +// !$*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 */; }; + 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 = ""; }; +/* 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 */, + ); + 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 */, + ); + 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 */, + ); + 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)"; + DEVELOPMENT_TEAM = 6WA62GTJNA; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.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/django-todolist/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/demos/django-todolist/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/demos/django-todolist/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/demos/django-todolist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demos/django-todolist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/demos/django-todolist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/demos/django-todolist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/demos/django-todolist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/demos/django-todolist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/demos/django-todolist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demos/django-todolist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..5e31d3d3 --- /dev/null +++ b/demos/django-todolist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/django-todolist/ios/Runner.xcworkspace/contents.xcworkspacedata b/demos/django-todolist/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/demos/django-todolist/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/demos/django-todolist/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demos/django-todolist/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/demos/django-todolist/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/demos/django-todolist/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/demos/django-todolist/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/demos/django-todolist/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/demos/django-todolist/ios/Runner/AppDelegate.swift b/demos/django-todolist/ios/Runner/AppDelegate.swift new file mode 100644 index 00000000..70693e4a --- /dev/null +++ b/demos/django-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/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/demos/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d36b1fab --- /dev/null +++ b/demos/django-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/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/demos/django-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/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/demos/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/demos/django-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/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/demos/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/demos/django-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/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/demos/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/demos/django-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/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/demos/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/demos/django-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/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/demos/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/demos/django-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/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/demos/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/demos/django-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/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/demos/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/demos/django-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/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/demos/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/demos/django-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/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/demos/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/demos/django-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/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/demos/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/demos/django-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/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/demos/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/demos/django-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/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/demos/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/demos/django-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/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/demos/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/demos/django-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/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/demos/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/demos/django-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/django-todolist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/demos/django-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/demos/django-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 00000000..0bedcf2f --- /dev/null +++ b/demos/django-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/django-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/demos/django-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/demos/django-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/demos/django-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/demos/django-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/demos/django-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/demos/django-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/demos/django-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/demos/django-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/demos/django-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/demos/django-todolist/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 00000000..89c2725b --- /dev/null +++ b/demos/django-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/django-todolist/ios/Runner/Base.lproj/LaunchScreen.storyboard b/demos/django-todolist/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..f2e259c7 --- /dev/null +++ b/demos/django-todolist/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/django-todolist/ios/Runner/Base.lproj/Main.storyboard b/demos/django-todolist/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f3c28516 --- /dev/null +++ b/demos/django-todolist/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/django-todolist/ios/Runner/Info.plist b/demos/django-todolist/ios/Runner/Info.plist new file mode 100644 index 00000000..fc3bb480 --- /dev/null +++ b/demos/django-todolist/ios/Runner/Info.plist @@ -0,0 +1,55 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + 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 + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + NSCameraUsageDescription + Use for todos + NSMicrophoneUsageDescription + Use for todos + + diff --git a/demos/django-todolist/ios/Runner/Runner-Bridging-Header.h b/demos/django-todolist/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 00000000..308a2a56 --- /dev/null +++ b/demos/django-todolist/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/demos/django-todolist/lib/api_client.dart b/demos/django-todolist/lib/api_client.dart new file mode 100644 index 00000000..0cfd287c --- /dev/null +++ b/demos/django-todolist/lib/api_client.dart @@ -0,0 +1,61 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:logging/logging.dart'; + +final log = Logger('powersync-django-todolist'); + +class ApiClient { + final String baseUrl; + + ApiClient(this.baseUrl); + + Future> authenticate( + String username, String password) async { + final response = await http.post( + Uri.parse('$baseUrl/api/auth/'), + headers: {'Content-Type': 'application/json'}, + body: json.encode({'username': username, 'password': password}), + ); + if (response.statusCode == 200) { + return json.decode(response.body); + } else { + throw Exception('Failed to authenticate'); + } + } + + Future> getToken(String userId) async { + final response = await http.get( + Uri.parse('$baseUrl/api/get_powersync_token/'), + headers: {'Content-Type': 'application/json'}, + ); + if (response.statusCode == 200) { + return json.decode(response.body); + } else { + throw Exception('Failed to fetch token'); + } + } + + Future upsert(Map record) async { + await http.put( + Uri.parse('$baseUrl/api/upload_data/'), + headers: {'Content-Type': 'application/json'}, + body: json.encode(record), + ); + } + + Future update(Map record) async { + await http.patch( + Uri.parse('$baseUrl/api/upload_data/'), + headers: {'Content-Type': 'application/json'}, + body: json.encode(record), + ); + } + + Future delete(Map record) async { + await http.delete( + Uri.parse('$baseUrl/api/upload_data/'), + headers: {'Content-Type': 'application/json'}, + body: json.encode(record), + ); + } +} diff --git a/demos/django-todolist/lib/app_config_template.dart b/demos/django-todolist/lib/app_config_template.dart new file mode 100644 index 00000000..33ed86fe --- /dev/null +++ b/demos/django-todolist/lib/app_config_template.dart @@ -0,0 +1,7 @@ +// Copy this template: `cp lib/app_config_template.dart lib/app_config.dart` +// Edit lib/app_config.dart and enter your Django and PowerSync project details. +class AppConfig { + // These are defaults when using the [self-host local demo](https://github.com/powersync-ja/self-host-demo/tree/main/demos/django) + static const String djangoUrl = 'http://localhost:6061'; + static const String powersyncUrl = 'http://localhost:8080'; +} diff --git a/demos/django-todolist/lib/fts_helpers.dart b/demos/django-todolist/lib/fts_helpers.dart new file mode 100644 index 00000000..a820bd2d --- /dev/null +++ b/demos/django-todolist/lib/fts_helpers.dart @@ -0,0 +1,17 @@ +import 'package:powersync_django_todolist_demo/powersync.dart'; + +String _createSearchTermWithOptions(String searchTerm) { + // adding * to the end of the search term will match any word that starts with the search term + // e.g. searching bl will match blue, black, etc. + // consult FTS5 Full-text Query Syntax documentation for more options + String searchTermWithOptions = '$searchTerm*'; + return searchTermWithOptions; +} + +/// Search the FTS table for the given searchTerm +Future search(String searchTerm, String tableName) async { + String searchTermWithOptions = _createSearchTermWithOptions(searchTerm); + return await db.getAll( + 'SELECT * FROM fts_$tableName WHERE fts_$tableName MATCH ? ORDER BY rank', + [searchTermWithOptions]); +} diff --git a/demos/django-todolist/lib/main.dart b/demos/django-todolist/lib/main.dart new file mode 100644 index 00000000..5fad8f77 --- /dev/null +++ b/demos/django-todolist/lib/main.dart @@ -0,0 +1,124 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:powersync_django_todolist_demo/models/schema.dart'; + +import './powersync.dart'; +import './widgets/lists_page.dart'; +import './widgets/login_page.dart'; +import './widgets/query_widget.dart'; +import './widgets/status_app_bar.dart'; + +void main() async { + 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 = await 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(); + +class MyApp extends StatelessWidget { + final bool loggedIn; + + const MyApp({super.key, required this.loggedIn}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'PowerSync Django Todolist 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: 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/django-todolist/lib/migrations/fts_setup.dart b/demos/django-todolist/lib/migrations/fts_setup.dart new file mode 100644 index 00000000..063d8484 --- /dev/null +++ b/demos/django-todolist/lib/migrations/fts_setup.dart @@ -0,0 +1,76 @@ +import 'package:powersync/powersync.dart'; +import 'package:sqlite_async/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/django-todolist/lib/migrations/helpers.dart b/demos/django-todolist/lib/migrations/helpers.dart new file mode 100644 index 00000000..c1a779e1 --- /dev/null +++ b/demos/django-todolist/lib/migrations/helpers.dart @@ -0,0 +1,38 @@ +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/django-todolist/lib/models/schema.dart b/demos/django-todolist/lib/models/schema.dart new file mode 100644 index 00000000..e07f51fc --- /dev/null +++ b/demos/django-todolist/lib/models/schema.dart @@ -0,0 +1,20 @@ +import 'package:powersync/powersync.dart'; + +const todosTable = 'todos'; + +Schema schema = const Schema(([ + Table(todosTable, [ + 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/django-todolist/lib/models/todo_item.dart b/demos/django-todolist/lib/models/todo_item.dart new file mode 100644 index 00000000..e31efea4 --- /dev/null +++ b/demos/django-todolist/lib/models/todo_item.dart @@ -0,0 +1,50 @@ +import 'package:powersync_django_todolist_demo/models/schema.dart'; + +import '../powersync.dart'; +import 'package:powersync/sqlite3_common.dart' as sqlite; + +/// TodoItem 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 String? photoId; + final bool completed; + + TodoItem( + {required this.id, + required this.description, + required this.completed, + required this.photoId}); + + factory TodoItem.fromRow(sqlite.Row row) { + return TodoItem( + id: row['id'], + description: row['description'], + photoId: row['photo_id'], + completed: row['completed'] == 1); + } + + Future toggle() async { + if (completed) { + await db.execute( + 'UPDATE $todosTable SET completed = FALSE, completed_by = NULL, completed_at = NULL WHERE id = ?', + [id]); + } else { + await db.execute( + 'UPDATE $todosTable SET completed = TRUE, completed_by = ?, completed_at = datetime() WHERE id = ?', + [await getUserId(), id]); + } + } + + Future delete() async { + await db.execute('DELETE FROM $todosTable WHERE id = ?', [id]); + } + + static Future addPhoto(String photoId, String id) async { + await db.execute( + 'UPDATE $todosTable SET photo_id = ? WHERE id = ?', [photoId, id]); + } +} diff --git a/demos/django-todolist/lib/models/todo_list.dart b/demos/django-todolist/lib/models/todo_list.dart new file mode 100644 index 00000000..643db070 --- /dev/null +++ b/demos/django-todolist/lib/models/todo_list.dart @@ -0,0 +1,103 @@ +import 'package:powersync/sqlite3_common.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, id') + .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 results = await db.execute(''' + INSERT INTO + lists(id, created_at, name, owner_id) + VALUES(uuid(), datetime(), ?, ?) + RETURNING * + ''', [name, await getUserId()]); + 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, id', + 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]); + } + + /// Find list item. + static Future find(id) async { + final results = await db.get('SELECT * FROM lists WHERE id = ?', [id]); + return TodoList.fromRow(results); + } + + /// 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, await getUserId()]); + return TodoItem.fromRow(results.first); + } +} diff --git a/demos/django-todolist/lib/powersync.dart b/demos/django-todolist/lib/powersync.dart new file mode 100644 index 00000000..bc6ecbb1 --- /dev/null +++ b/demos/django-todolist/lib/powersync.dart @@ -0,0 +1,140 @@ +// This file performs setup of the PowerSync database +import 'package:logging/logging.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:powersync/powersync.dart'; +import 'package:powersync_django_todolist_demo/migrations/fts_setup.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:powersync_django_todolist_demo/api_client.dart'; + +import './app_config.dart'; +import './models/schema.dart'; + +final log = Logger('powersync-django'); + +/// 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$'), +]; + +class DjangoConnector extends PowerSyncBackendConnector { + PowerSyncDatabase db; + + DjangoConnector(this.db); + + final ApiClient apiClient = ApiClient(AppConfig.djangoUrl); + + /// Get a token to authenticate against the PowerSync instance. + @override + Future fetchCredentials() async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('id'); + if (userId == null) { + throw Exception('User does not have session'); + } + // Somewhat contrived to illustrate usage, see auth docs here: + // https://docs.powersync.com/usage/installation/authentication-setup/custom + final session = await apiClient.getToken(userId); + return PowerSyncCredentials( + endpoint: AppConfig.powersyncUrl, token: session['token']); + } + + // Upload pending changes to Postgres via Django backend + @override + Future uploadData(PowerSyncDatabase database) async { + final transaction = await database.getNextCrudTransaction(); + + if (transaction == null) { + return; + } + + try { + for (var op in transaction.crud) { + final record = { + 'table': op.table, + 'data': {'id': op.id, ...?op.opData}, + }; + + switch (op.op) { + case UpdateType.put: + await apiClient.upsert(record); + break; + case UpdateType.patch: + await apiClient.update(record); + break; + case UpdateType.delete: + await apiClient.delete(record); + break; + } + } + await transaction.complete(); + } on Exception catch (e) { + log.severe('Error uploading data', 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. + rethrow; + } + } +} + +/// Global reference to the database +late final PowerSyncDatabase db; + +// Hacky flag to ensure the database is only initialized once, better to do this with listeners +bool _dbInitialized = false; + +Future isLoggedIn() async { + final prefs = + await SharedPreferences.getInstance(); // Initialize SharedPreferences + final userId = prefs.getString('id'); + if (userId != null) { + return true; + } + return false; +} + +Future getDatabasePath() async { + final dir = await getApplicationSupportDirectory(); + return join(dir.path, 'powersync-demo.db'); +} + +Future openDatabase() async { + // Open the local database + if (!_dbInitialized) { + db = PowerSyncDatabase( + schema: schema, path: await getDatabasePath(), logger: attachedLogger); + await db.initialize(); + + // Demo using SQLite Full-Text Search with PowerSync. + // See https://docs.powersync.com/usage-examples/full-text-search for more details + await configureFts(db); + _dbInitialized = true; + } + + DjangoConnector? currentConnector; + + if (await isLoggedIn()) { + // If the user is already logged in, connect immediately. + // Otherwise, connect once logged in. + currentConnector = DjangoConnector(db); + db.connect(connector: currentConnector); + } +} + +/// Explicit sign out - clear database and log out. +Future logout() async { + await db.disconnectAndClear(); +} + +/// id of the user currently logged in +Future getUserId() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString('id'); +} diff --git a/demos/django-todolist/lib/widgets/fts_search_delegate.dart b/demos/django-todolist/lib/widgets/fts_search_delegate.dart new file mode 100644 index 00000000..8cae351a --- /dev/null +++ b/demos/django-todolist/lib/widgets/fts_search_delegate.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:powersync_django_todolist_demo/fts_helpers.dart' as fts_helpers; +import 'package:powersync_django_todolist_demo/models/todo_list.dart'; + +import './todo_list_page.dart'; + +final log = Logger('powersync-django-todolist'); + +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 { + TodoList list = + await TodoList.find(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 { + 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/django-todolist/lib/widgets/list_item.dart b/demos/django-todolist/lib/widgets/list_item.dart new file mode 100644 index 00000000..981b382a --- /dev/null +++ b/demos/django-todolist/lib/widgets/list_item.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +import './todo_list_page.dart'; +import '../models/todo_list.dart'; + +class ListItemWidget extends StatelessWidget { + ListItemWidget({ + 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))); + } + + 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.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/django-todolist/lib/widgets/list_item_dialog.dart b/demos/django-todolist/lib/widgets/list_item_dialog.dart new file mode 100644 index 00000000..3fb8c133 --- /dev/null +++ b/demos/django-todolist/lib/widgets/list_item_dialog.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; + +import '../models/todo_list.dart'; + +class ListItemDialog extends StatefulWidget { + const ListItemDialog({super.key}); + + @override + State createState() { + return _ListItemDialogState(); + } +} + +class _ListItemDialogState extends State { + final TextEditingController _textFieldController = TextEditingController(); + + _ListItemDialogState(); + + @override + void dispose() { + super.dispose(); + _textFieldController.dispose(); + } + + Future add() async { + await TodoList.create(_textFieldController.text); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Add a new list'), + content: TextField( + controller: _textFieldController, + decoration: const InputDecoration(hintText: 'List name'), + onSubmitted: (value) async { + Navigator.of(context).pop(); + await add(); + }, + autofocus: true, + ), + actions: [ + OutlinedButton( + child: const Text('Cancel'), + onPressed: () { + _textFieldController.clear(); + Navigator.of(context).pop(); + }, + ), + ElevatedButton( + child: const Text('Create'), + onPressed: () async { + Navigator.of(context).pop(); + await add(); + }, + ), + ], + ); + } +} diff --git a/demos/django-todolist/lib/widgets/lists_page.dart b/demos/django-todolist/lib/widgets/lists_page.dart new file mode 100644 index 00000000..e31c2fc8 --- /dev/null +++ b/demos/django-todolist/lib/widgets/lists_page.dart @@ -0,0 +1,88 @@ +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'; + +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 = []; + 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(), + ); + } +} diff --git a/demos/django-todolist/lib/widgets/login_page.dart b/demos/django-todolist/lib/widgets/login_page.dart new file mode 100644 index 00000000..e2667d09 --- /dev/null +++ b/demos/django-todolist/lib/widgets/login_page.dart @@ -0,0 +1,143 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:powersync_django_todolist_demo/api_client.dart'; +import 'package:powersync_django_todolist_demo/app_config.dart'; +import 'package:powersync_django_todolist_demo/powersync.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../main.dart'; + +class LoginPage extends StatefulWidget { + const LoginPage({super.key}); + + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + late TextEditingController _passwordController; + late TextEditingController _usernameController; + String? _error; + late bool _busy; + + @override + void initState() { + super.initState(); + + _busy = false; + _passwordController = TextEditingController(text: ''); + _usernameController = TextEditingController(text: ''); + } + + void _login(BuildContext context) async { + final ApiClient apiClient = ApiClient(AppConfig.djangoUrl); + + setState(() { + _busy = true; + _error = null; + }); + try { + final session = await apiClient.authenticate( + _usernameController.text, _passwordController.text); + + final payload = _parseJwt(session['access_token']); + if (payload.containsKey('sub')) { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('id', payload['sub'].toString()); + + //re-init PowerSync manually for first time sign in + await openDatabase(); + } else { + setState(() { + _error = 'Invalid token payload'; + }); + } + + if (context.mounted) { + Navigator.of(context).pushReplacement(MaterialPageRoute( + builder: (context) => listsPage, + )); + } + } on Exception catch (e) { + setState(() { + _error = e.toString(); + }); + } catch (e) { + setState(() { + _error = e.toString(); + }); + } finally { + setState(() { + _busy = false; + }); + } + } + + Map _parseJwt(String token) { + final parts = token.split('.'); + final payload = parts[1]; + final normalized = base64Url.normalize(payload); + final decoded = utf8.decode(base64Url.decode(normalized)); + return json.decode(decoded); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("PowerSync Django Todolist Demo"), + ), + body: Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(30), + child: Center( + child: SizedBox( + width: 300, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text('Login'), + const SizedBox(height: 35), + TextFormField( + controller: _usernameController, + decoration: const InputDecoration(labelText: "Email"), + enabled: !_busy, + onFieldSubmitted: _busy + ? null + : (String value) { + _login(context); + }, + ), + const SizedBox(height: 20), + TextFormField( + obscureText: true, + controller: _passwordController, + decoration: InputDecoration( + labelText: "Password", errorText: _error), + enabled: !_busy, + onFieldSubmitted: _busy + ? null + : (String value) { + _login(context); + }, + ), + const SizedBox(height: 25), + TextButton( + onPressed: _busy + ? null + : () { + _login(context); + }, + child: const Text('Login'), + ), + ], + ), + ), + ), + ), + ), + )); + } +} diff --git a/demos/django-todolist/lib/widgets/query_widget.dart b/demos/django-todolist/lib/widgets/query_widget.dart new file mode 100644 index 00000000..426c7060 --- /dev/null +++ b/demos/django-todolist/lib/widgets/query_widget.dart @@ -0,0 +1,99 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:powersync/sqlite3_common.dart' as sqlite; + +import './resultset_table.dart'; +import '../powersync.dart'; + +class QueryWidget extends StatefulWidget { + final String defaultQuery; + + const QueryWidget({super.key, required this.defaultQuery}); + + @override + State createState() { + return QueryWidgetState(); + } +} + +class QueryWidgetState extends State { + sqlite.ResultSet? _data; + late TextEditingController _controller; + late String _query; + String? _error; + StreamSubscription? _subscription; + + QueryWidgetState(); + + @override + void initState() { + super.initState(); + _error = null; + _controller = TextEditingController(text: widget.defaultQuery); + _query = _controller.text; + _refresh(); + } + + @override + void dispose() { + super.dispose(); + _controller.dispose(); + _subscription?.cancel(); + } + + _refresh() async { + _subscription?.cancel(); + final stream = db.watch(_query); + _subscription = stream.listen((data) { + if (!context.mounted) { + return; + } + setState(() { + _data = data; + _error = null; + }); + }, onError: (e) { + setState(() { + if (e is sqlite.SqliteException) { + _error = "${e.message}!"; + } else { + _error = e.toString(); + } + }); + }); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: TextField( + controller: _controller, + onEditingComplete: () { + setState(() { + _query = _controller.text; + _refresh(); + }); + }, + decoration: InputDecoration( + isDense: false, + border: const OutlineInputBorder(), + labelText: 'Query', + errorText: _error), + ), + ), + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: ResultSetTable(data: _data), + ), + )) + ], + ); + } +} diff --git a/demos/django-todolist/lib/widgets/resultset_table.dart b/demos/django-todolist/lib/widgets/resultset_table.dart new file mode 100644 index 00000000..f348e4ff --- /dev/null +++ b/demos/django-todolist/lib/widgets/resultset_table.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:powersync/sqlite3_common.dart' as sqlite; + +/// Stateless DataTable rendering results from a SQLite query +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/django-todolist/lib/widgets/status_app_bar.dart b/demos/django-todolist/lib/widgets/status_app_bar.dart new file mode 100644 index 00000000..90d18ae8 --- /dev/null +++ b/demos/django-todolist/lib/widgets/status_app_bar.dart @@ -0,0 +1,94 @@ +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; + + @override + State createState() => _StatusAppBarState(); + + @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 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), + ], + ); + } +} + +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/django-todolist/lib/widgets/todo_item_dialog.dart b/demos/django-todolist/lib/widgets/todo_item_dialog.dart new file mode 100644 index 00000000..c12646c4 --- /dev/null +++ b/demos/django-todolist/lib/widgets/todo_item_dialog.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; + +import '../models/todo_list.dart'; + +class TodoItemDialog extends StatefulWidget { + final TodoList list; + + const TodoItemDialog({super.key, required this.list}); + + @override + State createState() { + return _TodoItemDialogState(); + } +} + +class _TodoItemDialogState extends State { + final TextEditingController _textFieldController = TextEditingController(); + + _TodoItemDialogState(); + + @override + void dispose() { + super.dispose(); + _textFieldController.dispose(); + } + + Future add() async { + Navigator.of(context).pop(); + + await widget.list.add(_textFieldController.text); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Add a new todo item'), + content: TextField( + controller: _textFieldController, + decoration: const InputDecoration(hintText: 'Type your new todo'), + onSubmitted: (value) { + add(); + }, + autofocus: true, + ), + actions: [ + OutlinedButton( + child: const Text('Cancel'), + onPressed: () { + _textFieldController.clear(); + Navigator.of(context).pop(); + }, + ), + ElevatedButton( + onPressed: add, + child: const Text('Add'), + ), + ], + ); + } +} diff --git a/demos/django-todolist/lib/widgets/todo_item_widget.dart b/demos/django-todolist/lib/widgets/todo_item_widget.dart new file mode 100644 index 00000000..054e6567 --- /dev/null +++ b/demos/django-todolist/lib/widgets/todo_item_widget.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; + +import '../models/todo_item.dart'; + +class TodoItemWidget extends StatelessWidget { + 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(TodoItem todo) async { + await todo.delete(); + } + + @override + Widget build(BuildContext context) { + return ListTile( + onTap: todo.toggle, + leading: Checkbox( + value: todo.completed, + onChanged: (_) { + todo.toggle(); + }, + ), + title: Row( + children: [ + Expanded( + child: Text(todo.description, + style: _getTextStyle(todo.completed))), + IconButton( + iconSize: 30, + icon: const Icon( + Icons.delete, + color: Colors.red, + ), + alignment: Alignment.centerRight, + onPressed: () async => await deleteTodo(todo), + tooltip: 'Delete Item', + ), + ], + )); + } +} diff --git a/demos/django-todolist/lib/widgets/todo_list_page.dart b/demos/django-todolist/lib/widgets/todo_list_page.dart new file mode 100644 index 00000000..26dbae63 --- /dev/null +++ b/demos/django-todolist/lib/widgets/todo_list_page.dart @@ -0,0 +1,89 @@ +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'; +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: list.name), + floatingActionButton: button, + body: TodoListWidget(list: list)); + } +} + +class TodoListWidget extends StatefulWidget { + 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(), + ); + } +} diff --git a/demos/django-todolist/linux/.gitignore b/demos/django-todolist/linux/.gitignore new file mode 100644 index 00000000..d3896c98 --- /dev/null +++ b/demos/django-todolist/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/demos/django-todolist/linux/CMakeLists.txt b/demos/django-todolist/linux/CMakeLists.txt new file mode 100644 index 00000000..df0a3887 --- /dev/null +++ b/demos/django-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/django-todolist/linux/flutter/CMakeLists.txt b/demos/django-todolist/linux/flutter/CMakeLists.txt new file mode 100644 index 00000000..d5bd0164 --- /dev/null +++ b/demos/django-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/django-todolist/linux/flutter/generated_plugin_registrant.cc b/demos/django-todolist/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..d0399125 --- /dev/null +++ b/demos/django-todolist/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) sqlite3_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); + sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); +} diff --git a/demos/django-todolist/linux/flutter/generated_plugin_registrant.h b/demos/django-todolist/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..e0f0a47b --- /dev/null +++ b/demos/django-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/django-todolist/linux/flutter/generated_plugins.cmake b/demos/django-todolist/linux/flutter/generated_plugins.cmake new file mode 100644 index 00000000..e2ef548c --- /dev/null +++ b/demos/django-todolist/linux/flutter/generated_plugins.cmake @@ -0,0 +1,25 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + powersync_flutter_libs + sqlite3_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/demos/django-todolist/linux/main.cc b/demos/django-todolist/linux/main.cc new file mode 100644 index 00000000..e7c5c543 --- /dev/null +++ b/demos/django-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/django-todolist/linux/my_application.cc b/demos/django-todolist/linux/my_application.cc new file mode 100644 index 00000000..7dcb7e37 --- /dev/null +++ b/demos/django-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/django-todolist/linux/my_application.h b/demos/django-todolist/linux/my_application.h new file mode 100644 index 00000000..72271d5e --- /dev/null +++ b/demos/django-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/django-todolist/macos/.gitignore b/demos/django-todolist/macos/.gitignore new file mode 100644 index 00000000..746adbb6 --- /dev/null +++ b/demos/django-todolist/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/demos/django-todolist/macos/Flutter/Flutter-Debug.xcconfig b/demos/django-todolist/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 00000000..4b81f9b2 --- /dev/null +++ b/demos/django-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/django-todolist/macos/Flutter/Flutter-Release.xcconfig b/demos/django-todolist/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 00000000..5caa9d15 --- /dev/null +++ b/demos/django-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/django-todolist/macos/Flutter/GeneratedPluginRegistrant.swift b/demos/django-todolist/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 00000000..ee254106 --- /dev/null +++ b/demos/django-todolist/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,18 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import path_provider_foundation +import powersync_flutter_libs +import shared_preferences_foundation +import sqlite3_flutter_libs + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + 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")) +} diff --git a/demos/django-todolist/macos/Podfile b/demos/django-todolist/macos/Podfile new file mode 100644 index 00000000..c795730d --- /dev/null +++ b/demos/django-todolist/macos/Podfile @@ -0,0 +1,43 @@ +platform :osx, '10.14' + +# 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/django-todolist/macos/Podfile.lock b/demos/django-todolist/macos/Podfile.lock new file mode 100644 index 00000000..67953800 --- /dev/null +++ b/demos/django-todolist/macos/Podfile.lock @@ -0,0 +1,67 @@ +PODS: + - FlutterMacOS (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - powersync-sqlite-core (0.1.6) + - powersync_flutter_libs (0.0.1): + - FlutterMacOS + - powersync-sqlite-core (~> 0.1.6) + - 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): + - FlutterMacOS + - "sqlite3 (~> 3.46.0+1)" + - sqlite3/dbstatvtab + - sqlite3/fts5 + - 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`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`) + +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 + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + sqlite3_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos + +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 + +PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 + +COCOAPODS: 1.15.2 diff --git a/demos/django-todolist/macos/Runner.xcodeproj/project.pbxproj b/demos/django-todolist/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..fe47ba76 --- /dev/null +++ b/demos/django-todolist/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,834 @@ +// !$*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 */; }; +/* 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 = ""; }; + 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 */, + ); + 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 = 1510; + 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 */, + ); + 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/django-todolist/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demos/django-todolist/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/demos/django-todolist/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/demos/django-todolist/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demos/django-todolist/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..2c526589 --- /dev/null +++ b/demos/django-todolist/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/django-todolist/macos/Runner.xcworkspace/contents.xcworkspacedata b/demos/django-todolist/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/demos/django-todolist/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/demos/django-todolist/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demos/django-todolist/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/demos/django-todolist/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/demos/django-todolist/macos/Runner/AppDelegate.swift b/demos/django-todolist/macos/Runner/AppDelegate.swift new file mode 100644 index 00000000..d53ef643 --- /dev/null +++ b/demos/django-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/django-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/demos/django-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..a2ec33f1 --- /dev/null +++ b/demos/django-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/django-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/demos/django-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/django-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/demos/django-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/demos/django-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/django-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/demos/django-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/demos/django-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/django-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/demos/django-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/demos/django-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/django-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/demos/django-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/demos/django-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/django-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/demos/django-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/demos/django-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/django-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/demos/django-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/demos/django-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/django-todolist/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/demos/django-todolist/macos/Runner/Base.lproj/MainMenu.xib b/demos/django-todolist/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 00000000..80e867a4 --- /dev/null +++ b/demos/django-todolist/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/django-todolist/macos/Runner/Configs/AppInfo.xcconfig b/demos/django-todolist/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 00000000..797d44b3 --- /dev/null +++ b/demos/django-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 Django 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/django-todolist/macos/Runner/Configs/Debug.xcconfig b/demos/django-todolist/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 00000000..36b0fd94 --- /dev/null +++ b/demos/django-todolist/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/demos/django-todolist/macos/Runner/Configs/Release.xcconfig b/demos/django-todolist/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 00000000..dff4f495 --- /dev/null +++ b/demos/django-todolist/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/demos/django-todolist/macos/Runner/Configs/Warnings.xcconfig b/demos/django-todolist/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 00000000..42bcbf47 --- /dev/null +++ b/demos/django-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/django-todolist/macos/Runner/DebugProfile.entitlements b/demos/django-todolist/macos/Runner/DebugProfile.entitlements new file mode 100644 index 00000000..08c3ab17 --- /dev/null +++ b/demos/django-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/django-todolist/macos/Runner/Info.plist b/demos/django-todolist/macos/Runner/Info.plist new file mode 100644 index 00000000..4789daa6 --- /dev/null +++ b/demos/django-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/django-todolist/macos/Runner/MainFlutterWindow.swift b/demos/django-todolist/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 00000000..2722837e --- /dev/null +++ b/demos/django-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/django-todolist/macos/Runner/Release.entitlements b/demos/django-todolist/macos/Runner/Release.entitlements new file mode 100644 index 00000000..ee95ab7e --- /dev/null +++ b/demos/django-todolist/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/demos/django-todolist/pubspec.lock b/demos/django-todolist/pubspec.lock new file mode 100644 index 00000000..39e98a00 --- /dev/null +++ b/demos/django-todolist/pubspec.lock @@ -0,0 +1,520 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + 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" + 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" + ffi: + dependency: transitive + description: + name: ffi + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + 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" + http: + dependency: "direct main" + description: + name: http + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + 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 + url: "https://pub.dev" + source: hosted + version: "0.7.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + url: "https://pub.dev" + source: hosted + version: "10.0.4" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + 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: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + url: "https://pub.dev" + source: hosted + version: "0.8.0" + meta: + dependency: transitive + description: + name: meta + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + url: "https://pub.dev" + source: hosted + version: "1.12.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: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: bca87b0165ffd7cdb9cad8edd22d18d2201e886d9a9f19b4fb3452ea7df3a72a + url: "https://pub.dev" + source: hosted + version: "2.2.6" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + url: "https://pub.dev" + source: hosted + version: "2.4.0" + 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: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + platform: + dependency: transitive + description: + name: platform + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + powersync: + dependency: "direct main" + description: + path: "../../packages/powersync" + relative: true + source: path + version: "1.5.4" + powersync_flutter_libs: + dependency: "direct overridden" + description: + path: "../../packages/powersync_flutter_libs" + relative: true + source: path + version: "0.1.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + url: "https://pub.dev" + source: hosted + version: "2.2.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + 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: "6d17989c0b06a5870b2190d391925186f944cb943e5262d0d3f778fcfca3bc6e" + url: "https://pub.dev" + source: hosted + version: "2.4.4" + sqlite3_flutter_libs: + dependency: transitive + description: + name: sqlite3_flutter_libs + sha256: "62bbb4073edbcdf53f40c80775f33eea01d301b7b81417e5b3fb7395416258c1" + url: "https://pub.dev" + source: hosted + version: "0.5.24" + sqlite3_web: + dependency: transitive + description: + name: sqlite3_web + sha256: "51fec34757577841cc72d79086067e3651c434669d5af557a5c106787198a76f" + url: "https://pub.dev" + source: hosted + version: "0.1.2-wip" + sqlite_async: + dependency: "direct main" + description: + name: sqlite_async + sha256: "79e636c857ed43f6cd5e5be72b36967a29f785daa63ff5b078bd34f74f44cb54" + url: "https://pub.dev" + source: hosted + version: "0.8.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + 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: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" + url: "https://pub.dev" + source: hosted + version: "4.4.0" + 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: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + url: "https://pub.dev" + source: hosted + version: "14.2.1" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + win32: + dependency: transitive + description: + name: win32 + sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 + url: "https://pub.dev" + source: hosted + version: "5.5.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" +sdks: + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.22.0" diff --git a/demos/django-todolist/pubspec.yaml b/demos/django-todolist/pubspec.yaml new file mode 100644 index 00000000..34b24079 --- /dev/null +++ b/demos/django-todolist/pubspec.yaml @@ -0,0 +1,28 @@ +name: powersync_django_todolist_demo +description: PowerSync Django Todolist Demo +publish_to: "none" + +version: 1.0.1 + +environment: + sdk: ^3.4.0 + +dependencies: + flutter: + sdk: flutter + powersync: ^1.5.5 + path_provider: ^2.1.1 + path: ^1.8.3 + logging: ^1.2.0 + sqlite_async: ^0.8.1 + http: ^1.2.1 + shared_preferences: ^2.2.3 + +dev_dependencies: + flutter_test: + sdk: flutter + + flutter_lints: ^3.0.1 + +flutter: + uses-material-design: true diff --git a/demos/django-todolist/windows/.gitignore b/demos/django-todolist/windows/.gitignore new file mode 100644 index 00000000..d492d0d9 --- /dev/null +++ b/demos/django-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/django-todolist/windows/CMakeLists.txt b/demos/django-todolist/windows/CMakeLists.txt new file mode 100644 index 00000000..ccfc4498 --- /dev/null +++ b/demos/django-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/django-todolist/windows/flutter/CMakeLists.txt b/demos/django-todolist/windows/flutter/CMakeLists.txt new file mode 100644 index 00000000..930d2071 --- /dev/null +++ b/demos/django-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/django-todolist/windows/flutter/generated_plugin_registrant.cc b/demos/django-todolist/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..673ae544 --- /dev/null +++ b/demos/django-todolist/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/demos/django-todolist/windows/flutter/generated_plugin_registrant.h b/demos/django-todolist/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..dc139d85 --- /dev/null +++ b/demos/django-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/django-todolist/windows/flutter/generated_plugins.cmake b/demos/django-todolist/windows/flutter/generated_plugins.cmake new file mode 100644 index 00000000..cdcfae6f --- /dev/null +++ b/demos/django-todolist/windows/flutter/generated_plugins.cmake @@ -0,0 +1,25 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + powersync_flutter_libs + sqlite3_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/demos/django-todolist/windows/runner/CMakeLists.txt b/demos/django-todolist/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..394917c0 --- /dev/null +++ b/demos/django-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/django-todolist/windows/runner/Runner.rc b/demos/django-todolist/windows/runner/Runner.rc new file mode 100644 index 00000000..75674c07 --- /dev/null +++ b/demos/django-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/django-todolist/windows/runner/flutter_window.cpp b/demos/django-todolist/windows/runner/flutter_window.cpp new file mode 100644 index 00000000..b25e363e --- /dev/null +++ b/demos/django-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/django-todolist/windows/runner/flutter_window.h b/demos/django-todolist/windows/runner/flutter_window.h new file mode 100644 index 00000000..6da0652f --- /dev/null +++ b/demos/django-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/django-todolist/windows/runner/main.cpp b/demos/django-todolist/windows/runner/main.cpp new file mode 100644 index 00000000..3eee96b7 --- /dev/null +++ b/demos/django-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/django-todolist/windows/runner/resource.h b/demos/django-todolist/windows/runner/resource.h new file mode 100644 index 00000000..66a65d1e --- /dev/null +++ b/demos/django-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/django-todolist/windows/runner/resources/app_icon.ico b/demos/django-todolist/windows/runner/resources/app_icon.ico new file mode 100644 index 00000000..c04e20ca Binary files /dev/null and b/demos/django-todolist/windows/runner/resources/app_icon.ico differ diff --git a/demos/django-todolist/windows/runner/runner.exe.manifest b/demos/django-todolist/windows/runner/runner.exe.manifest new file mode 100644 index 00000000..a42ea768 --- /dev/null +++ b/demos/django-todolist/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/demos/django-todolist/windows/runner/utils.cpp b/demos/django-todolist/windows/runner/utils.cpp new file mode 100644 index 00000000..f5bf9fa0 --- /dev/null +++ b/demos/django-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/django-todolist/windows/runner/utils.h b/demos/django-todolist/windows/runner/utils.h new file mode 100644 index 00000000..3879d547 --- /dev/null +++ b/demos/django-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/django-todolist/windows/runner/win32_window.cpp b/demos/django-todolist/windows/runner/win32_window.cpp new file mode 100644 index 00000000..041a3855 --- /dev/null +++ b/demos/django-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/django-todolist/windows/runner/win32_window.h b/demos/django-todolist/windows/runner/win32_window.h new file mode 100644 index 00000000..c86632d8 --- /dev/null +++ b/demos/django-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/linux/flutter/generated_plugin_registrant.cc b/demos/supabase-anonymous-auth/linux/flutter/generated_plugin_registrant.cc index fc949e03..1bef6a30 100644 --- a/demos/supabase-anonymous-auth/linux/flutter/generated_plugin_registrant.cc +++ b/demos/supabase-anonymous-auth/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include @@ -14,6 +15,9 @@ 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); diff --git a/demos/supabase-anonymous-auth/linux/flutter/generated_plugins.cmake b/demos/supabase-anonymous-auth/linux/flutter/generated_plugins.cmake index c34d0786..ed77a1a0 100644 --- a/demos/supabase-anonymous-auth/linux/flutter/generated_plugins.cmake +++ b/demos/supabase-anonymous-auth/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST gtk + powersync_flutter_libs sqlite3_flutter_libs url_launcher_linux ) diff --git a/demos/supabase-anonymous-auth/macos/Flutter/GeneratedPluginRegistrant.swift b/demos/supabase-anonymous-auth/macos/Flutter/GeneratedPluginRegistrant.swift index 7cd103ea..0c6fd7ff 100644 --- a/demos/supabase-anonymous-auth/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/demos/supabase-anonymous-auth/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,7 @@ import Foundation import app_links import path_provider_foundation +import powersync_flutter_libs import shared_preferences_foundation import sqlite3_flutter_libs import url_launcher_macos @@ -14,6 +15,7 @@ import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) 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-anonymous-auth/pubspec.lock b/demos/supabase-anonymous-auth/pubspec.lock index 65f565f4..912f1426 100644 --- a/demos/supabase-anonymous-auth/pubspec.lock +++ b/demos/supabase-anonymous-auth/pubspec.lock @@ -172,10 +172,10 @@ packages: dependency: transitive description: name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "0.7.1" jwt_decode: dependency: transitive description: @@ -350,7 +350,14 @@ packages: path: "../../packages/powersync" relative: true source: path - version: "1.3.0-alpha.8" + version: "1.5.4" + powersync_flutter_libs: + dependency: "direct overridden" + description: + path: "../../packages/powersync_flutter_libs" + relative: true + source: path + version: "0.1.0" realtime_client: dependency: transitive description: @@ -464,10 +471,10 @@ packages: dependency: transitive description: name: sqlite3_flutter_libs - sha256: d6c31c8511c441d1f12f20b607343df1afe4eddf24a1cf85021677c8eea26060 + sha256: "62bbb4073edbcdf53f40c80775f33eea01d301b7b81417e5b3fb7395416258c1" url: "https://pub.dev" source: hosted - version: "0.5.20" + version: "0.5.24" sqlite3_web: dependency: transitive description: diff --git a/demos/supabase-anonymous-auth/pubspec.yaml b/demos/supabase-anonymous-auth/pubspec.yaml index 9641326c..45aea985 100644 --- a/demos/supabase-anonymous-auth/pubspec.yaml +++ b/demos/supabase-anonymous-auth/pubspec.yaml @@ -5,13 +5,13 @@ publish_to: "none" version: 1.0.1 environment: - sdk: ^3.2.3 + sdk: ^3.4.0 dependencies: flutter: sdk: flutter - powersync: 1.3.0-alpha.9 + powersync: ^1.5.5 path_provider: ^2.1.1 supabase_flutter: ^2.0.2 path: ^1.8.3 diff --git a/demos/supabase-anonymous-auth/windows/flutter/generated_plugin_registrant.cc b/demos/supabase-anonymous-auth/windows/flutter/generated_plugin_registrant.cc index 0aed3732..691e6fc2 100644 --- a/demos/supabase-anonymous-auth/windows/flutter/generated_plugin_registrant.cc +++ b/demos/supabase-anonymous-auth/windows/flutter/generated_plugin_registrant.cc @@ -7,12 +7,15 @@ #include "generated_plugin_registrant.h" #include +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { AppLinksPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("AppLinksPluginCApi")); + PowersyncFlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PowersyncFlutterLibsPlugin")); Sqlite3FlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/demos/supabase-anonymous-auth/windows/flutter/generated_plugins.cmake b/demos/supabase-anonymous-auth/windows/flutter/generated_plugins.cmake index 7fe63857..0d5e9159 100644 --- a/demos/supabase-anonymous-auth/windows/flutter/generated_plugins.cmake +++ b/demos/supabase-anonymous-auth/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links + powersync_flutter_libs sqlite3_flutter_libs url_launcher_windows ) diff --git a/demos/supabase-edge-function-auth/linux/flutter/generated_plugin_registrant.cc b/demos/supabase-edge-function-auth/linux/flutter/generated_plugin_registrant.cc index fc949e03..1bef6a30 100644 --- a/demos/supabase-edge-function-auth/linux/flutter/generated_plugin_registrant.cc +++ b/demos/supabase-edge-function-auth/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include @@ -14,6 +15,9 @@ 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); diff --git a/demos/supabase-edge-function-auth/linux/flutter/generated_plugins.cmake b/demos/supabase-edge-function-auth/linux/flutter/generated_plugins.cmake index c34d0786..ed77a1a0 100644 --- a/demos/supabase-edge-function-auth/linux/flutter/generated_plugins.cmake +++ b/demos/supabase-edge-function-auth/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST gtk + powersync_flutter_libs sqlite3_flutter_libs url_launcher_linux ) diff --git a/demos/supabase-edge-function-auth/macos/Flutter/GeneratedPluginRegistrant.swift b/demos/supabase-edge-function-auth/macos/Flutter/GeneratedPluginRegistrant.swift index 7cd103ea..0c6fd7ff 100644 --- a/demos/supabase-edge-function-auth/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/demos/supabase-edge-function-auth/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,7 @@ import Foundation import app_links import path_provider_foundation +import powersync_flutter_libs import shared_preferences_foundation import sqlite3_flutter_libs import url_launcher_macos @@ -14,6 +15,7 @@ import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) 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-edge-function-auth/pubspec.lock b/demos/supabase-edge-function-auth/pubspec.lock index 65f565f4..912f1426 100644 --- a/demos/supabase-edge-function-auth/pubspec.lock +++ b/demos/supabase-edge-function-auth/pubspec.lock @@ -172,10 +172,10 @@ packages: dependency: transitive description: name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "0.7.1" jwt_decode: dependency: transitive description: @@ -350,7 +350,14 @@ packages: path: "../../packages/powersync" relative: true source: path - version: "1.3.0-alpha.8" + version: "1.5.4" + powersync_flutter_libs: + dependency: "direct overridden" + description: + path: "../../packages/powersync_flutter_libs" + relative: true + source: path + version: "0.1.0" realtime_client: dependency: transitive description: @@ -464,10 +471,10 @@ packages: dependency: transitive description: name: sqlite3_flutter_libs - sha256: d6c31c8511c441d1f12f20b607343df1afe4eddf24a1cf85021677c8eea26060 + sha256: "62bbb4073edbcdf53f40c80775f33eea01d301b7b81417e5b3fb7395416258c1" url: "https://pub.dev" source: hosted - version: "0.5.20" + version: "0.5.24" sqlite3_web: dependency: transitive description: diff --git a/demos/supabase-edge-function-auth/pubspec.yaml b/demos/supabase-edge-function-auth/pubspec.yaml index 9cc8ba5b..0b47e1b6 100644 --- a/demos/supabase-edge-function-auth/pubspec.yaml +++ b/demos/supabase-edge-function-auth/pubspec.yaml @@ -5,13 +5,13 @@ publish_to: "none" version: 1.0.1 environment: - sdk: ^3.2.3 + sdk: ^3.4.0 dependencies: flutter: sdk: flutter - powersync: 1.3.0-alpha.9 + powersync: ^1.5.5 path_provider: ^2.1.1 supabase_flutter: ^2.0.2 path: ^1.8.3 diff --git a/demos/supabase-edge-function-auth/windows/flutter/generated_plugin_registrant.cc b/demos/supabase-edge-function-auth/windows/flutter/generated_plugin_registrant.cc index 0aed3732..691e6fc2 100644 --- a/demos/supabase-edge-function-auth/windows/flutter/generated_plugin_registrant.cc +++ b/demos/supabase-edge-function-auth/windows/flutter/generated_plugin_registrant.cc @@ -7,12 +7,15 @@ #include "generated_plugin_registrant.h" #include +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { AppLinksPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("AppLinksPluginCApi")); + PowersyncFlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PowersyncFlutterLibsPlugin")); Sqlite3FlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/demos/supabase-edge-function-auth/windows/flutter/generated_plugins.cmake b/demos/supabase-edge-function-auth/windows/flutter/generated_plugins.cmake index 7fe63857..0d5e9159 100644 --- a/demos/supabase-edge-function-auth/windows/flutter/generated_plugins.cmake +++ b/demos/supabase-edge-function-auth/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links + powersync_flutter_libs sqlite3_flutter_libs url_launcher_windows ) diff --git a/demos/supabase-simple-chat/lib/models/message.dart b/demos/supabase-simple-chat/lib/models/message.dart index da4c965e..28088b36 100644 --- a/demos/supabase-simple-chat/lib/models/message.dart +++ b/demos/supabase-simple-chat/lib/models/message.dart @@ -1,5 +1,5 @@ import '../powersync.dart'; -import 'package:powersync/sqlite3.dart' as sqlite; +import 'package:powersync/sqlite3_common.dart' as sqlite; class Message { Message({ diff --git a/demos/supabase-simple-chat/lib/models/profile.dart b/demos/supabase-simple-chat/lib/models/profile.dart index 6e18707e..7e24d6b2 100644 --- a/demos/supabase-simple-chat/lib/models/profile.dart +++ b/demos/supabase-simple-chat/lib/models/profile.dart @@ -1,4 +1,4 @@ -import 'package:powersync/sqlite3.dart' as sqlite; +import 'package:powersync/sqlite3_common.dart' as sqlite; import '../powersync.dart'; class Profile { diff --git a/demos/supabase-simple-chat/linux/flutter/generated_plugin_registrant.cc b/demos/supabase-simple-chat/linux/flutter/generated_plugin_registrant.cc index fc949e03..1bef6a30 100644 --- a/demos/supabase-simple-chat/linux/flutter/generated_plugin_registrant.cc +++ b/demos/supabase-simple-chat/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include @@ -14,6 +15,9 @@ 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); diff --git a/demos/supabase-simple-chat/linux/flutter/generated_plugins.cmake b/demos/supabase-simple-chat/linux/flutter/generated_plugins.cmake index c34d0786..ed77a1a0 100644 --- a/demos/supabase-simple-chat/linux/flutter/generated_plugins.cmake +++ b/demos/supabase-simple-chat/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST gtk + powersync_flutter_libs sqlite3_flutter_libs url_launcher_linux ) diff --git a/demos/supabase-simple-chat/macos/Flutter/GeneratedPluginRegistrant.swift b/demos/supabase-simple-chat/macos/Flutter/GeneratedPluginRegistrant.swift index c49b609b..0c6fd7ff 100644 --- a/demos/supabase-simple-chat/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/demos/supabase-simple-chat/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,16 +7,16 @@ import Foundation import app_links import path_provider_foundation +import powersync_flutter_libs import shared_preferences_foundation -import sign_in_with_apple import sqlite3_flutter_libs import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + PowersyncFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "PowersyncFlutterLibsPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) - SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/demos/supabase-simple-chat/pubspec.lock b/demos/supabase-simple-chat/pubspec.lock index 45900457..44358f5f 100644 --- a/demos/supabase-simple-chat/pubspec.lock +++ b/demos/supabase-simple-chat/pubspec.lock @@ -140,18 +140,18 @@ packages: dependency: transitive description: name: functions_client - sha256: "3b157b4d3ae9e38614fd80fab76d1ef1e0e39ff3412a45de2651f27cecb9d2d2" + sha256: "48659e5c6a4bbe02659102bf6406a0cf39142202deae65aacfa78688f2e68946" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "2.2.0" gotrue: dependency: transitive description: name: gotrue - sha256: f3a47cdbc59e543f453a1ef150050cd7650fe756254ac1fcac1d2a2f6f2b5a21 + sha256: a8784341bcc08f88ba7a4b04a40a37059c7e71c315f058d45c31d09e8a951194 url: "https://pub.dev" source: hosted - version: "1.12.6" + version: "2.8.3" gtk: dependency: transitive description: @@ -160,22 +160,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" - hive: - dependency: transitive - description: - name: hive - sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" - url: "https://pub.dev" - source: hosted - version: "2.2.3" - hive_flutter: - dependency: transitive - description: - name: hive_flutter - sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc - url: "https://pub.dev" - source: hosted - version: "1.1.0" http: dependency: transitive description: @@ -204,10 +188,10 @@ packages: dependency: transitive description: name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "0.7.1" jwt_decode: dependency: transitive description: @@ -372,25 +356,32 @@ packages: dependency: transitive description: name: postgrest - sha256: f190eddc5779842dfa529fa239ec4b1025f6f968c18052ba6fffc0aecac93e6b + sha256: f1f78470a74c611811132ff12acdef9c08b3ec65b61e88161a057d6cc5fbbd83 url: "https://pub.dev" source: hosted - version: "1.5.2" + version: "2.1.2" powersync: dependency: "direct main" description: path: "../../packages/powersync" relative: true source: path - version: "1.3.0-alpha.8" + version: "1.5.4" + powersync_flutter_libs: + dependency: "direct overridden" + description: + path: "../../packages/powersync_flutter_libs" + relative: true + source: path + version: "0.1.0" realtime_client: dependency: transitive description: name: realtime_client - sha256: "2027358cdbe65d5f1770c3f768aa9adecd394de486c5dbbd2cfe19d5c6dbbc4a" + sha256: a99b7817e203c57ada746e9fe113820410cf84d9029f4310c57737aae890b0f7 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "2.2.0" retry: dependency: transitive description: @@ -463,30 +454,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" - sign_in_with_apple: - dependency: transitive - description: - name: sign_in_with_apple - sha256: "0975c23b9f8b30a80e27d5659a75993a093d4cb5f4eb7d23a9ccc586fea634e0" - url: "https://pub.dev" - source: hosted - version: "5.0.0" - sign_in_with_apple_platform_interface: - dependency: transitive - description: - name: sign_in_with_apple_platform_interface - sha256: c2ef2ce6273fce0c61acd7e9ff5be7181e33d7aa2b66508b39418b786cca2119 - url: "https://pub.dev" - source: hosted - version: "1.1.0" - sign_in_with_apple_web: - dependency: transitive - description: - name: sign_in_with_apple_web - sha256: "44b66528f576e77847c14999d5e881e17e7223b7b0625a185417829e5306f47a" - url: "https://pub.dev" - source: hosted - version: "1.0.1" sky_engine: dependency: transitive description: flutter @@ -520,10 +487,10 @@ packages: dependency: transitive description: name: sqlite3_flutter_libs - sha256: "9f89a7e7dc36eac2035808427eba1c3fbd79e59c3a22093d8dace6d36b1fe89e" + sha256: "62bbb4073edbcdf53f40c80775f33eea01d301b7b81417e5b3fb7395416258c1" url: "https://pub.dev" source: hosted - version: "0.5.23" + version: "0.5.24" sqlite3_web: dependency: transitive description: @@ -552,10 +519,10 @@ packages: dependency: transitive description: name: storage_client - sha256: f02d4d8967bec77767dcaf9daf24ca5b8d5a9f1cc093f14dffb77930b52589a3 + sha256: e37f1b9d40f43078d12bd2d1b6b08c2c16fbdbafc58b57bc44922da6ea3f5625 url: "https://pub.dev" source: hosted - version: "1.5.4" + version: "2.0.2" stream_channel: dependency: transitive description: @@ -576,18 +543,18 @@ packages: dependency: transitive description: name: supabase - sha256: "1434bb9375f88f51802dadf7b99568117c434f6a9af7f8a55e5be94c8b4da7c9" + sha256: "7d2ca0499a6933b9aed2f4ff9eff4cbc4107f54006be35c42c8e1db70e99438e" url: "https://pub.dev" source: hosted - version: "1.11.11" + version: "2.2.4" supabase_flutter: dependency: "direct main" description: name: supabase_flutter - sha256: "8d68a4fa3215bc23811469fc3499c3895ebb35a2363d6edcfffaa426d5effd84" + sha256: "048d9377a76e43b95039d67e26b4bb2326fc8818df8d2f4bd63b4fdfc79f8f50" url: "https://pub.dev" source: hosted - version: "1.10.25" + version: "2.5.8" term_glyph: dependency: transitive description: @@ -732,38 +699,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.5" - webview_flutter: - dependency: transitive - description: - name: webview_flutter - sha256: "6869c8786d179f929144b4a1f86e09ac0eddfe475984951ea6c634774c16b522" - url: "https://pub.dev" - source: hosted - version: "4.8.0" - webview_flutter_android: - dependency: transitive - description: - name: webview_flutter_android - sha256: f42447ca49523f11d8f70abea55ea211b3cafe172dd7a0e7ac007bb35dd356dc - url: "https://pub.dev" - source: hosted - version: "3.16.4" - webview_flutter_platform_interface: - dependency: transitive - description: - name: webview_flutter_platform_interface - sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d - url: "https://pub.dev" - source: hosted - version: "2.10.0" - webview_flutter_wkwebview: - dependency: transitive - description: - name: webview_flutter_wkwebview - sha256: "7affdf9d680c015b11587181171d3cad8093e449db1f7d9f0f08f4f33d24f9a0" - url: "https://pub.dev" - source: hosted - version: "3.13.1" win32: dependency: transitive description: @@ -784,10 +719,10 @@ packages: dependency: transitive description: name: yet_another_json_isolate - sha256: "86fad76026c4241a32831d6c7febd8f9bded5019e2cd36c5b148499808d8307d" + sha256: e727502a2640d65b4b8a8a6cb48af9dd0cbe644ba4b3ee667c7f4afa0c1d6069 url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "2.0.0" sdks: dart: ">=3.4.0 <4.0.0" flutter: ">=3.22.0" diff --git a/demos/supabase-simple-chat/pubspec.yaml b/demos/supabase-simple-chat/pubspec.yaml index b1c5a023..0ba9d9c0 100644 --- a/demos/supabase-simple-chat/pubspec.yaml +++ b/demos/supabase-simple-chat/pubspec.yaml @@ -35,9 +35,9 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.6 - supabase_flutter: ^1.10.25 + supabase_flutter: ^2.0.2 timeago: ^3.6.0 - powersync: 1.3.0-alpha.9 + powersync: ^1.5.5 path_provider: ^2.1.1 path: ^1.8.3 logging: ^1.2.0 diff --git a/demos/supabase-simple-chat/windows/flutter/generated_plugin_registrant.cc b/demos/supabase-simple-chat/windows/flutter/generated_plugin_registrant.cc index 0aed3732..691e6fc2 100644 --- a/demos/supabase-simple-chat/windows/flutter/generated_plugin_registrant.cc +++ b/demos/supabase-simple-chat/windows/flutter/generated_plugin_registrant.cc @@ -7,12 +7,15 @@ #include "generated_plugin_registrant.h" #include +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { AppLinksPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("AppLinksPluginCApi")); + PowersyncFlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PowersyncFlutterLibsPlugin")); Sqlite3FlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/demos/supabase-simple-chat/windows/flutter/generated_plugins.cmake b/demos/supabase-simple-chat/windows/flutter/generated_plugins.cmake index 7fe63857..0d5e9159 100644 --- a/demos/supabase-simple-chat/windows/flutter/generated_plugins.cmake +++ b/demos/supabase-simple-chat/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links + powersync_flutter_libs sqlite3_flutter_libs url_launcher_windows ) diff --git a/demos/supabase-todolist/android/app/build.gradle b/demos/supabase-todolist/android/app/build.gradle index b0ab28b2..9daa778b 100644 --- a/demos/supabase-todolist/android/app/build.gradle +++ b/demos/supabase-todolist/android/app/build.gradle @@ -46,7 +46,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 21 + minSdkVersion 24 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/demos/supabase-todolist/ios/Podfile.lock b/demos/supabase-todolist/ios/Podfile.lock index 82c7af5d..3440e3db 100644 --- a/demos/supabase-todolist/ios/Podfile.lock +++ b/demos/supabase-todolist/ios/Podfile.lock @@ -7,21 +7,28 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - powersync-sqlite-core (0.1.6) + - powersync_flutter_libs (0.0.1): + - Flutter + - powersync-sqlite-core (~> 0.1.6) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - sqlite3 (3.46.0): - - sqlite3/common (= 3.46.0) - - sqlite3/common (3.46.0) - - sqlite3/fts5 (3.46.0): + - "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): + - "sqlite3/perf-threadsafe (3.46.0+1)": - sqlite3/common - - sqlite3/rtree (3.46.0): + - "sqlite3/rtree (3.46.0+1)": - sqlite3/common - sqlite3_flutter_libs (0.0.1): - Flutter - - sqlite3 (~> 3.46.0) + - "sqlite3 (~> 3.46.0+1)" + - sqlite3/dbstatvtab - sqlite3/fts5 - sqlite3/perf-threadsafe - sqlite3/rtree @@ -33,12 +40,14 @@ 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`) - 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: @@ -50,6 +59,8 @@ 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" sqlite3_flutter_libs: @@ -62,9 +73,11 @@ SPEC CHECKSUMS: camera_avfoundation: 759172d1a77ae7be0de08fc104cfb79738b8a59e Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + powersync-sqlite-core: 4c38c8f470f6dca61346789fd5436a6826d1e3dd + powersync_flutter_libs: 5d6b132a398de442c0853a8b14bfbb62cd4ff5a1 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sqlite3: 154b084339ede06960a5b3c8160066adc9176b7d - sqlite3_flutter_libs: 0d611efdf6d1c9297d5ab03dab21b75aeebdae31 + sqlite3: 292c3e1bfe89f64e51ea7fc7dab9182a017c8630 + sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe PODFILE CHECKSUM: f7b3cb7384a2d5da4b22b090e1f632de7f377987 diff --git a/demos/supabase-todolist/lib/models/todo_list.dart b/demos/supabase-todolist/lib/models/todo_list.dart index e871b6b6..86170e02 100644 --- a/demos/supabase-todolist/lib/models/todo_list.dart +++ b/demos/supabase-todolist/lib/models/todo_list.dart @@ -1,3 +1,4 @@ +import 'package:powersync/powersync.dart'; import 'package:powersync/sqlite3_common.dart' as sqlite; import './todo_item.dart'; @@ -59,6 +60,10 @@ class TodoList { }); } + static Stream watchSyncStatus() { + return db.statusStream; + } + /// Create a new list static Future create(String name) async { final results = await db.execute(''' diff --git a/demos/supabase-todolist/lib/widgets/lists_page.dart b/demos/supabase-todolist/lib/widgets/lists_page.dart index e31c2fc8..142d9e9f 100644 --- a/demos/supabase-todolist/lib/widgets/lists_page.dart +++ b/demos/supabase-todolist/lib/widgets/lists_page.dart @@ -52,7 +52,9 @@ class ListsWidget extends StatefulWidget { class _ListsWidgetState extends State { List _data = []; + bool hasSynced = false; StreamSubscription? _subscription; + StreamSubscription? _syncStatusSubscription; _ListsWidgetState(); @@ -68,21 +70,32 @@ class _ListsWidgetState extends State { _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 ListView( - padding: const EdgeInsets.symmetric(vertical: 8.0), - children: _data.map((list) { - return ListItemWidget(list: list); - }).toList(), - ); + 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/linux/flutter/generated_plugin_registrant.cc b/demos/supabase-todolist/linux/flutter/generated_plugin_registrant.cc index fc949e03..1bef6a30 100644 --- a/demos/supabase-todolist/linux/flutter/generated_plugin_registrant.cc +++ b/demos/supabase-todolist/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include @@ -14,6 +15,9 @@ 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); diff --git a/demos/supabase-todolist/linux/flutter/generated_plugins.cmake b/demos/supabase-todolist/linux/flutter/generated_plugins.cmake index c34d0786..ed77a1a0 100644 --- a/demos/supabase-todolist/linux/flutter/generated_plugins.cmake +++ b/demos/supabase-todolist/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST gtk + powersync_flutter_libs sqlite3_flutter_libs url_launcher_linux ) diff --git a/demos/supabase-todolist/macos/Flutter/GeneratedPluginRegistrant.swift b/demos/supabase-todolist/macos/Flutter/GeneratedPluginRegistrant.swift index 7cd103ea..0c6fd7ff 100644 --- a/demos/supabase-todolist/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/demos/supabase-todolist/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,7 @@ import Foundation import app_links import path_provider_foundation +import powersync_flutter_libs import shared_preferences_foundation import sqlite3_flutter_libs import url_launcher_macos @@ -14,6 +15,7 @@ import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) 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-todolist/macos/Podfile.lock b/demos/supabase-todolist/macos/Podfile.lock index a7d30ad6..5c75a944 100644 --- a/demos/supabase-todolist/macos/Podfile.lock +++ b/demos/supabase-todolist/macos/Podfile.lock @@ -5,21 +5,28 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - powersync-sqlite-core (0.1.6) + - powersync_flutter_libs (0.0.1): + - FlutterMacOS + - powersync-sqlite-core (~> 0.1.6) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - "sqlite3 (3.45.3+1)": - - "sqlite3/common (= 3.45.3+1)" - - "sqlite3/common (3.45.3+1)" - - "sqlite3/fts5 (3.45.3+1)": + - "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.45.3+1)": + - "sqlite3/perf-threadsafe (3.46.0+1)": - sqlite3/common - - "sqlite3/rtree (3.45.3+1)": + - "sqlite3/rtree (3.46.0+1)": - sqlite3/common - sqlite3_flutter_libs (0.0.1): - FlutterMacOS - - "sqlite3 (~> 3.45.3+1)" + - "sqlite3 (~> 3.46.0+1)" + - sqlite3/dbstatvtab - sqlite3/fts5 - sqlite3/perf-threadsafe - sqlite3/rtree @@ -30,12 +37,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`) - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) SPEC REPOS: trunk: + - powersync-sqlite-core - sqlite3 EXTERNAL SOURCES: @@ -45,6 +54,8 @@ 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 sqlite3_flutter_libs: @@ -56,9 +67,11 @@ SPEC CHECKSUMS: app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + powersync-sqlite-core: 4c38c8f470f6dca61346789fd5436a6826d1e3dd + powersync_flutter_libs: 1eb1c6790a72afe08e68d4cc489d71ab61da32ee shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 - sqlite3: 02d1f07eaaa01f80a1c16b4b31dfcbb3345ee01a - sqlite3_flutter_libs: 8d204ef443cf0d5c1c8b058044eab53f3943a9c5 + sqlite3: 292c3e1bfe89f64e51ea7fc7dab9182a017c8630 + sqlite3_flutter_libs: 5ca46c1a04eddfbeeb5b16566164aa7ad1616e7b url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 diff --git a/demos/supabase-todolist/pubspec.lock b/demos/supabase-todolist/pubspec.lock index b264a16c..87a00874 100644 --- a/demos/supabase-todolist/pubspec.lock +++ b/demos/supabase-todolist/pubspec.lock @@ -268,10 +268,10 @@ packages: dependency: transitive description: name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "0.7.1" jwt_decode: dependency: transitive description: @@ -454,14 +454,21 @@ packages: path: "../../packages/powersync" relative: true source: path - version: "1.3.0-alpha.8" + version: "1.5.4" powersync_attachments_helper: dependency: "direct main" description: path: "../../packages/powersync_attachments_helper" relative: true source: path - version: "0.3.0-alpha.3" + version: "0.5.1" + powersync_flutter_libs: + dependency: "direct overridden" + description: + path: "../../packages/powersync_flutter_libs" + relative: true + source: path + version: "0.1.0" realtime_client: dependency: transitive description: diff --git a/demos/supabase-todolist/pubspec.yaml b/demos/supabase-todolist/pubspec.yaml index 13098a00..c38e8523 100644 --- a/demos/supabase-todolist/pubspec.yaml +++ b/demos/supabase-todolist/pubspec.yaml @@ -5,14 +5,13 @@ publish_to: "none" version: 1.0.1 environment: - sdk: ^3.2.3 + sdk: ^3.4.0 dependencies: flutter: sdk: flutter - powersync_attachments_helper: ^0.3.0-alpha.4 - - powersync: 1.3.0-alpha.9 + powersync_attachments_helper: ^0.5.1 + powersync: ^1.5.5 path_provider: ^2.1.1 supabase_flutter: ^2.0.1 path: ^1.8.3 diff --git a/demos/supabase-todolist/windows/flutter/generated_plugin_registrant.cc b/demos/supabase-todolist/windows/flutter/generated_plugin_registrant.cc index 0aed3732..691e6fc2 100644 --- a/demos/supabase-todolist/windows/flutter/generated_plugin_registrant.cc +++ b/demos/supabase-todolist/windows/flutter/generated_plugin_registrant.cc @@ -7,12 +7,15 @@ #include "generated_plugin_registrant.h" #include +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { AppLinksPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("AppLinksPluginCApi")); + PowersyncFlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PowersyncFlutterLibsPlugin")); Sqlite3FlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/demos/supabase-todolist/windows/flutter/generated_plugins.cmake b/demos/supabase-todolist/windows/flutter/generated_plugins.cmake index 7fe63857..0d5e9159 100644 --- a/demos/supabase-todolist/windows/flutter/generated_plugins.cmake +++ b/demos/supabase-todolist/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links + powersync_flutter_libs sqlite3_flutter_libs url_launcher_windows ) diff --git a/melos.yaml b/melos.yaml index ec9aca75..a50b9c3a 100644 --- a/melos.yaml +++ b/melos.yaml @@ -9,7 +9,7 @@ ide: intellij: false scripts: - prepare: melos bootstrap && dart ./scripts/compile_webworker.dart && dart ./scripts/init_sqlite_wasm.dart + prepare: melos bootstrap && dart ./scripts/compile_webworker.dart && dart ./scripts/init_sqlite_wasm.dart && dart ./scripts/init_powersync_core_binary.dart analyze:demos: description: Analyze Dart code in demos. @@ -46,7 +46,7 @@ scripts: test: description: Run tests in a specific package. - run: dart test -p vm,chrome + run: flutter test exec: concurrency: 1 packageFilters: @@ -57,4 +57,15 @@ scripts: env: MELOS_TEST: true + test:web: + description: Run web tests in a specific package. + run: dart test -p chrome + exec: + concurrency: 1 + packageFilters: + dirExists: + - test + env: + MELOS_TEST: true + update:wasm: dart run scripts/init_sqlite_wasm.dart diff --git a/packages/powersync/CHANGELOG.md b/packages/powersync/CHANGELOG.md index 3ad1af70..7499e87f 100644 --- a/packages/powersync/CHANGELOG.md +++ b/packages/powersync/CHANGELOG.md @@ -1,3 +1,44 @@ +## 1.5.5 + + - Fix issue where `hasSynced` is cleared when offline. + +## 1.5.4 + +- Fix watch query parameter `triggerOnTables` to prepend powersync view names. +- Upgrade dependency `sqlite_async` to version 0.8.1. + +## 1.5.3 + +- Added support for client parameters when connecting. + +## 1.5.2 + +- Refactor `waitForFirstSync()` to iterate through the stream and remove the use of a `Future`. +- Fix sync connection not immediately closed when calling `db.disconnect()` (#114). + +## 1.5.1 + +- Adds a hasSynced flag to check if initial data has been synced. +- Adds a waitForFirstSync method to check if the first full sync has completed. + +## 1.5.0 + +- Upgrade minimum Dart SDK constraint to `3.4.0`. +- Upgrade `sqlite_async` to version 0.7.0 which updates all Database types to use a `CommonDatabase` interface. + +## 1.4.2 + +- Fix `Bad state: Future already completed` error when calling `disconnectAndClear()`. + +## 1.4.1 + +- Upgrades dependency `powersync_flutter_libs` to version `0.1.0`. + +## 1.4.0 + +- Introduces the use of the `powersync-sqlite-core` native extension. This is our common Rust core which means all PowerSync SDKs now use the same core logic for PowerSync functionality, improving maintainability and support. +- Added a new package dependency on `powersync_flutter_libs` for loading the extension. + ## 1.3.0-alpha.9 - Updated sqlite_async to use Navigator locks for limiting sync stream implementations in multiple tabs diff --git a/packages/powersync/README.md b/packages/powersync/README.md index b5ece0d1..3d576be9 100644 --- a/packages/powersync/README.md +++ b/packages/powersync/README.md @@ -4,9 +4,9 @@ # PowerSync SDK for Dart/Flutter -[PowerSync](https://powersync.com) is a service and set of SDKs that keeps Postgres databases in sync with on-device SQLite databases. +_[PowerSync](https://www.powersync.com) is a Postgres-SQLite sync layer, which helps developers to create local-first real-time reactive apps that work seamlessly both online and offline._ -This package (`powersync`) is the PowerSync SDK for Dart/Flutter clients. +This package (`powersync`) is the PowerSync client SDK for Dart/Flutter. See a summary of features [here](https://docs.powersync.com/client-sdk-references/flutter). @@ -51,7 +51,7 @@ The latest prerelease version can be found [here](https://pub.dev/packages/power Web support requires `sqlite3.wasm` and `powersync_db.worker.js` assets to be served from the web application. This is typically achieved by placing the files in the project `web` directory. -- `sqlite3.wasm` can be found [here](https://github.com/simolus3/sqlite3.dart/releases) +- `sqlite3.wasm` can be found [here](https://github.com/powersync-ja/sqlite3.dart/releases/download/v0.1.0/sqlite3.wasm) - `powersync_db.worker.js` can be found in the repo's [releases](https://github.com/powersync-ja/powersync.dart/releases) page. Currently the Drift SQLite library is used under the hood for DB connections. See [here](https://drift.simonbinder.eu/web/#getting-started) for detailed compatibility diff --git a/packages/powersync/lib/sqlite3_common.dart b/packages/powersync/lib/sqlite3_common.dart index 0d73acf4..df84a8e0 100644 --- a/packages/powersync/lib/sqlite3_common.dart +++ b/packages/powersync/lib/sqlite3_common.dart @@ -1,4 +1,4 @@ -/// Re-exports [sqlite3](https://pub.dev/packages/sqlite3) to expose sqlite3 without +/// Re-exports [sqlite3_common](https://pub.dev/packages/sqlite3) to expose sqlite3_common without /// adding it as a direct dependency. library; diff --git a/packages/powersync/lib/src/bucket_storage.dart b/packages/powersync/lib/src/bucket_storage.dart index b08c097f..2781f1d4 100644 --- a/packages/powersync/lib/src/bucket_storage.dart +++ b/packages/powersync/lib/src/bucket_storage.dart @@ -2,15 +2,12 @@ import 'dart:async'; import 'dart:convert'; import 'package:collection/collection.dart'; -import 'package:meta/meta.dart'; import 'package:powersync/sqlite_async.dart'; -import 'package:powersync/sqlite3_common.dart' as sqlite; +import 'package:powersync/sqlite3_common.dart'; import 'crud.dart'; -import 'schema_helpers.dart'; +import 'schema_logic.dart'; import 'sync_types.dart'; -import 'uuid.dart'; -import 'log_internal.dart'; const compactOperationInterval = 1000; @@ -18,36 +15,21 @@ class BucketStorage { final SqliteConnection _internalDb; bool _hasCompletedSync = false; bool _pendingBucketDeletes = false; - Set tableNames = {}; int _compactCounter = compactOperationInterval; - ChecksumCache? _checksumCache; - late Future _isInitialized; BucketStorage(SqliteConnection db) : _internalDb = db { - _isInitialized = _init(); + _init(); } - _init() async { - final existingTableRows = await select( - "SELECT name FROM sqlite_master WHERE type='table' AND name GLOB 'ps_data_*'"); - for (final row in existingTableRows) { - tableNames.add(row['name'] as String); - } - } - - initialized() { - return _isInitialized; - } + _init() {} // Use only for read statements - Future select(String query, + Future select(String query, [List parameters = const []]) async { - return _internalDb.execute(query, parameters); + return await _internalDb.execute(query, parameters); } - void startSession() { - _checksumCache = null; - } + void startSession() {} Future> getBucketStates() async { final rows = await select( @@ -58,155 +40,33 @@ class BucketStorage { ]; } + Future streamOp(String op) async { + await writeTransaction((tx) async { + await tx.execute( + 'INSERT INTO powersync_operations(op, data) VALUES(?, ?)', + ['stream', op]); + }); + } + Future saveSyncData(SyncDataBatch batch) async { var count = 0; await writeTransaction((tx) async { for (var b in batch.buckets) { - var bucket = b.bucket; - var data = b.data; - - count += data.length; - final isFinal = !b.hasMore; - await _updateBucket(tx, bucket, data, isFinal); + count += b.data.length; + await _updateBucket2( + tx, + jsonEncode({ + 'buckets': [b] + })); } }); _compactCounter += count; } - Future _updateBucket(SqliteWriteContext tx, String bucket, - List data, bool finalBucketUpdate) async { - if (data.isEmpty) { - return; - } - - String? lastOp; - String? firstOp; - BigInt? targetOp; - - List> inserts = []; - Map> lastInsert = {}; - List allEntries = []; - - List clearOps = []; - - for (final op in data) { - lastOp = op.opId; - firstOp ??= op.opId; - - final Map insert = { - 'op_id': op.opId, - 'op': op.op!.value, - 'bucket': bucket, - 'key': op.key, - 'row_type': op.rowType, - 'row_id': op.rowId, - 'data': op.data, - 'checksum': op.checksum, - 'superseded': 0 - }; - - if (op.op == OpType.move) { - insert['superseded'] = 1; - } - - if (op.op == OpType.put || - op.op == OpType.remove || - op.op == OpType.move) { - inserts.add(insert); - } - - if (op.op == OpType.put || op.op == OpType.remove) { - final key = op.key; - final prev = lastInsert[key]; - if (prev != null) { - prev['superseded'] = 1; - } - lastInsert[key] = insert; - allEntries.add(key); - } else if (op.op == OpType.move) { - final target = op.parsedData?['target'] as String?; - if (target != null) { - final l = BigInt.parse(target, radix: 10); - if (targetOp == null || l < targetOp) { - targetOp = l; - } - } - } else if (op.op == OpType.clear) { - // Any remaining PUT operations should get an implicit REMOVE. - clearOps.add(SqliteOp( - "UPDATE ps_oplog SET op=${OpType.remove.value}, data=NULL, hash=0 WHERE (op=${OpType.put.value} OR op=${OpType.remove.value}) AND bucket=? AND op_id <= ?", - [bucket, op.opId])); - // And we need to re-apply all of those. - // We also replace the checksum with the checksum of the CLEAR op. - clearOps.add(SqliteOp( - "UPDATE ps_buckets SET last_applied_op = 0, add_checksum = ? WHERE name = ?", - [op.checksum, bucket])); - } - } - - // Mark old ops as superseded - await tx.execute(""" - UPDATE ps_oplog AS oplog - SET superseded = 1, - op = ${OpType.move.value}, - data = NULL - WHERE oplog.superseded = 0 - AND unlikely(oplog.bucket = ?) - AND oplog.key IN (SELECT json_each.value FROM json_each(?)) - """, [bucket, jsonEncode(allEntries)]); - - await tx.executeBatch( - 'INSERT INTO ps_oplog(op_id, op, bucket, key, row_type, row_id, data, hash, superseded) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', - inserts - .map((insert) => [ - insert['op_id'], - insert['op'], - insert['bucket'], - insert['key'], - insert['row_type'], - insert['row_id'], - insert['data'], - insert['checksum'], - insert['superseded'] - ]) - .toList()); - - await tx - .execute("INSERT OR IGNORE INTO ps_buckets(name) VALUES(?)", [bucket]); - - if (lastOp != null) { - await tx.execute( - "UPDATE ps_buckets SET last_op = ? WHERE name = ?", [lastOp, bucket]); - } - if (targetOp != null) { - await tx.execute( - "UPDATE ps_buckets AS buckets SET target_op = MAX(?, buckets.target_op) WHERE name = ?", - [targetOp.toString(), bucket]); - } - - for (final op in clearOps) { - await tx.execute(op.sql, op.args); - } - - // Compact superseded ops immediately, but only _after_ clearing - if (firstOp != null && lastOp != null) { - await tx.execute("""UPDATE ps_buckets AS buckets - SET add_checksum = add_checksum + (SELECT IFNULL(SUM(hash), 0) - FROM ps_oplog AS oplog - WHERE superseded = 1 - AND oplog.bucket = ? - AND oplog.op_id >= ? - AND oplog.op_id <= ?) - WHERE buckets.name = ?""", [bucket, firstOp, lastOp, bucket]); - - await tx.execute("""DELETE - FROM ps_oplog - WHERE superseded = 1 - AND bucket = ? - AND op_id >= ? - AND op_id <= ?""", [bucket, firstOp, lastOp]); - } + Future _updateBucket2(SqliteWriteContext tx, String json) async { + await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', + ['save', json]); } Future removeBuckets(List buckets) async { @@ -216,19 +76,10 @@ class BucketStorage { } Future deleteBucket(String bucket) async { - final newName = "\$delete_${bucket}_${uuid.v4()}"; - await writeTransaction((tx) async { await tx.execute( - "UPDATE ps_oplog SET op=${OpType.remove.value}, data=NULL WHERE op=${OpType.put.value} AND superseded=0 AND bucket=?", - [bucket]); - // Rename bucket - await tx.execute( - "UPDATE ps_oplog SET bucket=? WHERE bucket=?", [newName, bucket]); - await tx.execute("DELETE FROM ps_buckets WHERE name = ?", [bucket]); - await tx.execute( - "INSERT INTO ps_buckets(name, pending_delete, last_op) SELECT ?, 1, IFNULL(MAX(op_id), 0) FROM ps_oplog WHERE bucket = ?", - [newName, newName]); + 'INSERT INTO powersync_operations(op, data) VALUES(?, ?)', + ['delete_bucket', bucket]); }); _pendingBucketDeletes = true; @@ -282,236 +133,32 @@ class BucketStorage { Future updateObjectsFromBuckets(Checkpoint checkpoint) async { return writeTransaction((tx) async { - if (!(await canUpdateLocal(tx))) { + await tx.execute( + "INSERT INTO powersync_operations(op, data) VALUES(?, ?)", + ['sync_local', '']); + final rs = await tx.execute('SELECT last_insert_rowid() as result'); + final result = rs[0]['result']; + if (result == 1) { + return true; + } else { + // can_update_local(db) == false return false; } - - // Updated objects - // TODO: Reduce memory usage - // Some options here: - // 1. Create a VIEW objects_updates, which contains triggers to update individual tables. - // This works well for individual tables, but difficult to have a catch all for untyped data, - // and performance degrades when we have hundreds of object types. - // 2. Similar, but use a TEMP TABLE instead. We can then query those from JS, and populate the tables from JS. - // 3. Skip the temp table, and query the data directly. Sorting and limiting becomes tricky. - // 3a. LIMIT on the first oplog step. This prevents us from using JOIN after this. - // 3b. LIMIT after the second oplog query - - // QUERY PLAN - // |--SCAN buckets - // |--SEARCH b USING INDEX ps_oplog_by_opid (bucket=? AND op_id>?) - // |--SEARCH r USING INDEX ps_oplog_by_row (row_type=? AND row_id=?) - // `--USE TEMP B-TREE FOR GROUP BY - // language=DbSqlite - var opRows = await tx.execute( - """-- 3. Group the objects from different buckets together into a single one (ops). - SELECT r.row_type as type, - r.row_id as id, - r.data as data, - json_group_array(r.bucket) FILTER (WHERE r.op=${OpType.put.value}) as buckets, - /* max() affects which row is used for 'data' */ - max(r.op_id) FILTER (WHERE r.op=${OpType.put.value}) as op_id - -- 1. Filter oplog by the ops added but not applied yet (oplog b). - FROM ps_buckets AS buckets - CROSS JOIN ps_oplog AS b ON b.bucket = buckets.name - AND (b.op_id > buckets.last_applied_op) - -- 2. Find *all* current ops over different buckets for those objects (oplog r). - INNER JOIN ps_oplog AS r - ON r.row_type = b.row_type - AND r.row_id = b.row_id - WHERE r.superseded = 0 - AND b.superseded = 0 - -- Group for (3) - GROUP BY r.row_type, r.row_id - """); - - await saveOps(tx, opRows); - - await tx.execute("""UPDATE ps_buckets - SET last_applied_op = last_op - WHERE last_applied_op != last_op"""); - - isolateLogger.fine('Applied checkpoint ${checkpoint.lastOpId}'); - return true; }); } - // { type: string; id: string; data: string; buckets: string; op_id: string }[] - Future saveOps(SqliteWriteContext tx, List rows) async { - Map> byType = {}; - for (final row in rows) { - byType.putIfAbsent(row['type'], () => []).add(row); - } - - for (final entry in byType.entries) { - final type = entry.key; - final typeRows = entry.value; - final table = getTypeTableName(type); - - // Note that "PUT" and "DELETE" are split, and not applied in row order. - // So we only do either PUT or DELETE for each individual object, not both. - final Set removeIds = {}; - List puts = []; - for (final row in typeRows) { - if (row['buckets'] == '[]') { - removeIds.add(row['id']); - } else { - puts.add(row); - removeIds.remove(row['id']); - } - } - - puts = puts.where((update) => !removeIds.contains(update['id'])).toList(); - - if (tableNames.contains(table)) { - await tx.execute("""REPLACE INTO "$table"(id, data) - SELECT json_extract(json_each.value, '\$.id'), - json_extract(json_each.value, '\$.data') - FROM json_each(?)""", [jsonEncode(puts)]); - - await tx.execute("""DELETE - FROM "$table" - WHERE id IN (SELECT json_each.value FROM json_each(?))""", [ - jsonEncode([...removeIds]) - ]); - } else { - await tx.execute(r"""REPLACE INTO ps_untyped(type, id, data) - SELECT ?, - json_extract(json_each.value, '$.id'), - json_extract(json_each.value, '$.data') - FROM json_each(?)""", [type, jsonEncode(puts)]); - - await tx.execute("""DELETE FROM ps_untyped - WHERE type = ? - AND id IN (SELECT json_each.value FROM json_each(?))""", - [type, jsonEncode(removeIds.toList())]); - } - } - } - - @protected - Future canUpdateLocal(SqliteWriteContext tx) async { - final invalidBuckets = await tx.execute( - "SELECT name, CAST(target_op AS TEXT), last_op, last_applied_op FROM ps_buckets WHERE target_op > last_op AND (name = '\$local' OR pending_delete = 0)"); - if (invalidBuckets.isNotEmpty) { - if (invalidBuckets.first['name'] == '\$local') { - isolateLogger.fine('Waiting for local changes to be acknowledged'); - } else { - isolateLogger.fine('Waiting for more data: $invalidBuckets'); - } - return false; - } - // This is specifically relevant for when data is added to crud before another batch is completed. - final rows = await tx.execute('SELECT 1 FROM ps_crud LIMIT 1'); - if (rows.isNotEmpty) { - return false; - } - return true; - } - Future validateChecksums( Checkpoint checkpoint) async { - final rows = await select("""WITH - bucket_list(bucket, lower_op_id) AS ( - SELECT - json_extract(json_each.value, '\$.bucket') as bucket, - json_extract(json_each.value, '\$.last_op_id') as lower_op_id - FROM json_each(?) - ) - SELECT - buckets.name as bucket, - buckets.add_checksum as add_checksum, - IFNULL(SUM(oplog.hash), 0) as oplog_checksum, - COUNT(oplog.op_id) as count, - CAST(MAX(oplog.op_id) as TEXT) as last_op_id, - CAST(buckets.last_applied_op as TEXT) as last_applied_op - FROM bucket_list - LEFT OUTER JOIN ps_buckets AS buckets ON - buckets.name = bucket_list.bucket - LEFT OUTER JOIN ps_oplog AS oplog ON - bucket_list.bucket = oplog.bucket AND - oplog.op_id <= ? AND oplog.op_id > bucket_list.lower_op_id - GROUP BY bucket_list.bucket""", [ - jsonEncode(checkpoint.checksums - .map((checksum) => { - 'bucket': checksum.bucket, - 'last_op_id': - _checksumCache?.checksums[checksum.bucket]?.lastOpId ?? '0' - }) - .toList()), - checkpoint.lastOpId - ]); - - Map byBucket = {}; - if (_checksumCache != null) { - final checksums = _checksumCache!.checksums; - for (var row in rows) { - final String? bucket = row['bucket']; - if (bucket == null) { - continue; - } - if (BigInt.parse(row['last_applied_op']) > - BigInt.parse(_checksumCache!.lastOpId)) { - throw AssertionError( - "assertion failed: ${row['last_applied_op']} > ${_checksumCache!.lastOpId}"); - } - int checksum; - String? lastOpId = row['last_op_id']; - if (checksums.containsKey(bucket)) { - // All rows may have been filtered out, in which case we use the previous one - lastOpId ??= checksums[bucket]!.lastOpId; - checksum = - (checksums[bucket]!.checksum + row['oplog_checksum'] as int) - .toSigned(32); - } else { - checksum = (row['add_checksum'] + row['oplog_checksum']).toSigned(32); - } - byBucket[bucket] = BucketChecksum( - bucket: bucket, - checksum: checksum, - count: row['count'], - lastOpId: lastOpId); - } - } else { - for (final row in rows) { - final String? bucket = row['bucket']; - if (bucket == null) { - continue; - } - final int c1 = row['add_checksum']; - final int c2 = row['oplog_checksum']; - - final checksum = (c1 + c2).toSigned(32); - - byBucket[bucket] = BucketChecksum( - bucket: bucket, - checksum: checksum, - count: row['count'], - lastOpId: row['last_op_id']); - } - } - - List failedChecksums = []; - for (final checksum in checkpoint.checksums) { - final local = byBucket[checksum.bucket] ?? - BucketChecksum(bucket: checksum.bucket, checksum: 0, count: 0); - // Note: Count is informational only. - if (local.checksum != checksum.checksum) { - isolateLogger.warning( - 'Checksum mismatch for ${checksum.bucket}: local ${local.checksum} != remote ${checksum.checksum}. Likely due to sync rule changes.'); - failedChecksums.add(checksum.bucket); - } - } - if (failedChecksums.isEmpty) { - // FIXME: Checksum cache disabled since it's broken when add_checksum is modified - // _checksumCache = ChecksumCache(checkpoint.lastOpId, byBucket); + final rs = await select("SELECT powersync_validate_checkpoint(?) as result", + [jsonEncode(checkpoint)]); + final result = jsonDecode(rs[0]['result']); + if (result['valid']) { return SyncLocalDatabaseResult(ready: true); } else { - _checksumCache = null; return SyncLocalDatabaseResult( - ready: false, checkpointValid: false, - checkpointFailures: failedChecksums); + ready: false, + checkpointFailures: result['failed_buckets'].cast()); } } @@ -538,7 +185,7 @@ class BucketStorage { await writeTransaction((tx) async { await tx.execute('PRAGMA wal_checkpoint(TRUNCATE)'); }); - } on sqlite.SqliteException catch (e) { + } on SqliteException catch (e) { // Ignore SQLITE_BUSY if (e.resultCode == 5) { // Ignore @@ -553,9 +200,8 @@ class BucketStorage { // Executed once after start-up, and again when there are pending deletes. await writeTransaction((tx) async { await tx.execute( - 'DELETE FROM ps_oplog WHERE bucket IN (SELECT name FROM ps_buckets WHERE pending_delete = 1 AND last_applied_op = last_op AND last_op >= target_op)'); - await tx.execute( - 'DELETE FROM ps_buckets WHERE pending_delete = 1 AND last_applied_op = last_op AND last_op >= target_op'); + 'INSERT INTO powersync_operations(op, data) VALUES (?, ?)', + ['delete_pending_buckets', '']); }); _pendingBucketDeletes = false; } @@ -566,30 +212,11 @@ class BucketStorage { return; } - final rows = await select( - 'SELECT name, cast(last_applied_op as TEXT) as last_applied_op, cast(last_op as TEXT) as last_op FROM ps_buckets WHERE pending_delete = 0'); - for (var row in rows) { - await writeTransaction((tx) async { - // Note: The row values here may be different from when queried. That should not be an issue. - - await tx.execute("""UPDATE ps_buckets AS buckets - SET add_checksum = add_checksum + (SELECT IFNULL(SUM(hash), 0) - FROM ps_oplog AS oplog - WHERE (superseded = 1 OR op != ${OpType.put.value}) - AND oplog.bucket = ? - AND oplog.op_id <= ?) - WHERE buckets.name = ?""", - [row['name'], row['last_applied_op'], row['name']]); - await tx.execute( - """DELETE - FROM ps_oplog - WHERE (superseded = 1 OR op != ${OpType.put.value}) - AND bucket = ? - AND op_id <= ?""", - // Must use the same values as above - [row['name'], row['last_applied_op']]); - }); - } + await writeTransaction((tx) async { + await tx.execute( + 'INSERT INTO powersync_operations(op, data) VALUES (?, ?)', + ['clear_remove_ops', '']); + }); _compactCounter = 0; } @@ -736,6 +363,16 @@ class SyncBucketData { 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 { @@ -783,6 +420,18 @@ class OplogEntry { 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 { @@ -854,18 +503,19 @@ enum OpType { return null; } } -} -/// Get a table name for a specific type. The table may or may not exist. -/// -/// The table name must always be enclosed in "quotes" when using inside a SQL query. -/// -/// @param type -@protected -String getTypeTableName(String type) { - // Test for invalid characters rather than escaping. - if (invalidSqliteCharacters.hasMatch(type)) { - throw AssertionError("Invalid characters in type name: $type"); - } - return "ps_data__$type"; + String toJson() { + switch (this) { + case clear: + return 'CLEAR'; + case move: + return 'MOVE'; + case put: + return 'PUT'; + case remove: + return 'REMOVE'; + default: + return ''; + } + } } diff --git a/packages/powersync/lib/src/database/native/native_bucket_storage.dart b/packages/powersync/lib/src/database/native/native_bucket_storage.dart deleted file mode 100644 index d2ecd10a..00000000 --- a/packages/powersync/lib/src/database/native/native_bucket_storage.dart +++ /dev/null @@ -1,150 +0,0 @@ -import 'dart:convert'; - -import 'package:powersync/sqlite3_common.dart'; -import 'package:powersync/src/log_internal.dart'; -import 'package:powersync/src/sync_types.dart'; -import 'package:sqlite_async/sqlite3.dart' as sqlite; - -import 'package:powersync/src/bucket_storage.dart'; - -/// Native implementation for [BucketStorage] -/// Uses direct SQLite3 connection for memory and performance -/// optimizations. -class NativeBucketStorage extends BucketStorage { - NativeBucketStorage(super.db); - - @override - - /// Native specific version for updating objects from buckets - /// this uses the SQLite3 database directly for better memory usage. - Future updateObjectsFromBuckets(Checkpoint checkpoint) async { - // Internal connection is private, but can be accessed via a transaction - return writeTransaction((tx) { - return tx.computeWithDatabase((db) async { - if (!(await canUpdateLocal(tx))) { - return false; - } - - // Updated objects - // TODO: Reduce memory usage - // Some options here: - // 1. Create a VIEW objects_updates, which contains triggers to update individual tables. - // This works well for individual tables, but difficult to have a catch all for untyped data, - // and performance degrades when we have hundreds of object types. - // 2. Similar, but use a TEMP TABLE instead. We can then query those from JS, and populate the tables from JS. - // 3. Skip the temp table, and query the data directly. Sorting and limiting becomes tricky. - // 3a. LIMIT on the first oplog step. This prevents us from using JOIN after this. - // 3b. LIMIT after the second oplog query - - // QUERY PLAN - // |--SCAN buckets - // |--SEARCH b USING INDEX ps_oplog_by_opid (bucket=? AND op_id>?) - // |--SEARCH r USING INDEX ps_oplog_by_row (row_type=? AND row_id=?) - // `--USE TEMP B-TREE FOR GROUP BY - // language=DbSqlite - var stmt = db.prepare( - """-- 3. Group the objects from different buckets together into a single one (ops). - SELECT r.row_type as type, - r.row_id as id, - r.data as data, - json_group_array(r.bucket) FILTER (WHERE r.op=${OpType.put.value}) as buckets, - /* max() affects which row is used for 'data' */ - max(r.op_id) FILTER (WHERE r.op=${OpType.put.value}) as op_id - -- 1. Filter oplog by the ops added but not applied yet (oplog b). - FROM ps_buckets AS buckets - CROSS JOIN ps_oplog AS b ON b.bucket = buckets.name - AND (b.op_id > buckets.last_applied_op) - -- 2. Find *all* current ops over different buckets for those objects (oplog r). - INNER JOIN ps_oplog AS r - ON r.row_type = b.row_type - AND r.row_id = b.row_id - WHERE r.superseded = 0 - AND b.superseded = 0 - -- Group for (3) - GROUP BY r.row_type, r.row_id - """); - try { - // TODO: Perhaps we don't need batching for this? - var cursor = stmt.selectCursor([]); - List rows = []; - while (cursor.moveNext()) { - var row = cursor.current; - rows.add(row); - - if (rows.length >= 10000) { - _saveOps(db, rows); - rows = []; - } - } - if (rows.isNotEmpty) { - _saveOps(db, rows); - } - } finally { - stmt.dispose(); - } - - db.execute("""UPDATE ps_buckets - SET last_applied_op = last_op - WHERE last_applied_op != last_op"""); - - isolateLogger.fine('Applied checkpoint ${checkpoint.lastOpId}'); - return true; - }); - }); - } - - /// Native specific version of saveOps which operates directly - /// on the SQLite3 connection - /// { type: string; id: string; data: string; buckets: string; op_id: string }[] - void _saveOps(CommonDatabase db, List rows) { - Map> byType = {}; - for (final row in rows) { - byType.putIfAbsent(row['type'], () => []).add(row); - } - - for (final entry in byType.entries) { - final type = entry.key; - final typeRows = entry.value; - final table = getTypeTableName(type); - - // Note that "PUT" and "DELETE" are split, and not applied in row order. - // So we only do either PUT or DELETE for each individual object, not both. - final Set removeIds = {}; - List puts = []; - for (final row in typeRows) { - if (row['buckets'] == '[]') { - removeIds.add(row['id']); - } else { - puts.add(row); - removeIds.remove(row['id']); - } - } - - puts = puts.where((update) => !removeIds.contains(update['id'])).toList(); - - if (tableNames.contains(table)) { - db.execute("""REPLACE INTO "$table"(id, data) - SELECT json_extract(json_each.value, '\$.id'), - json_extract(json_each.value, '\$.data') - FROM json_each(?)""", [jsonEncode(puts)]); - - db.execute("""DELETE - FROM "$table" - WHERE id IN (SELECT json_each.value FROM json_each(?))""", [ - jsonEncode([...removeIds]) - ]); - } else { - db.execute(r"""REPLACE INTO ps_untyped(type, id, data) - SELECT ?, - json_extract(json_each.value, '$.id'), - json_extract(json_each.value, '$.data') - FROM json_each(?)""", [type, jsonEncode(puts)]); - - db.execute("""DELETE FROM ps_untyped - WHERE type = ? - AND id IN (SELECT json_each.value FROM json_each(?))""", - [type, jsonEncode(removeIds.toList())]); - } - } - } -} diff --git a/packages/powersync/lib/src/database/native/native_powersync_database.dart b/packages/powersync/lib/src/database/native/native_powersync_database.dart index 4044c0df..0bf859ed 100644 --- a/packages/powersync/lib/src/database/native/native_powersync_database.dart +++ b/packages/powersync/lib/src/database/native/native_powersync_database.dart @@ -5,8 +5,8 @@ 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/native/native_bucket_storage.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'; @@ -130,7 +130,9 @@ class PowerSyncDatabaseImpl /// Throttle time between CRUD operations /// Defaults to 10 milliseconds. - Duration crudThrottleTime = const Duration(milliseconds: 10)}) async { + required Duration crudThrottleTime, + required Future Function() reconnect, + Map? params}) async { await initialize(); // Disconnect if connected @@ -199,7 +201,9 @@ class PowerSyncDatabaseImpl logger.severe('Sync Isolate error', message); // Reconnect - connect(connector: connector, crudThrottleTime: crudThrottleTime); + // Use the param like this instead of directly calling connect(), to avoid recursive + // locks in some edge cases. + reconnect(); }); disconnected() { @@ -221,8 +225,10 @@ class PowerSyncDatabaseImpl return; } - Isolate.spawn(_powerSyncDatabaseIsolate, - _PowerSyncDatabaseIsolateArgs(rPort.sendPort, dbRef, retryDelay), + Isolate.spawn( + _powerSyncDatabaseIsolate, + _PowerSyncDatabaseIsolateArgs( + rPort.sendPort, dbRef, retryDelay, clientParams), debugName: 'PowerSyncDatabase', onError: errorPort.sendPort, onExit: exitPort.sendPort); @@ -264,8 +270,10 @@ class _PowerSyncDatabaseIsolateArgs { final SendPort sPort; final IsolateConnectionFactory dbRef; final Duration retryDelay; + final Map? parameters; - _PowerSyncDatabaseIsolateArgs(this.sPort, this.dbRef, this.retryDelay); + _PowerSyncDatabaseIsolateArgs( + this.sPort, this.dbRef, this.retryDelay, this.parameters); } Future _powerSyncDatabaseIsolate( @@ -277,6 +285,8 @@ Future _powerSyncDatabaseIsolate( CommonDatabase? db; final Mutex mutex = args.dbRef.mutex.open(); + StreamingSyncImplementation? openedStreamingSync; + rPort.listen((message) async { if (message is List) { String action = message[0]; @@ -290,6 +300,9 @@ Future _powerSyncDatabaseIsolate( db?.dispose(); updateController.close(); upstreamDbClient.close(); + // Abort any open http requests, and wait for it to be closed properly + await openedStreamingSync?.abort(); + // No kill the Isolate Isolate.current.kill(); } } @@ -329,7 +342,7 @@ Future _powerSyncDatabaseIsolate( .open(SqliteOpenOptions(primaryConnection: false, readOnly: false)); final connection = SyncSqliteConnection(db!, mutex); - final storage = NativeBucketStorage(connection); + final storage = BucketStorage(connection); final sync = StreamingSyncImplementation( adapter: storage, credentialsCallback: loadCredentials, @@ -337,7 +350,9 @@ Future _powerSyncDatabaseIsolate( uploadCrud: uploadCrud, updateStream: updateController.stream, retryDelay: args.retryDelay, - client: http.Client()); + client: http.Client(), + syncParameters: args.parameters); + openedStreamingSync = sync; sync.streamingSync(); sync.statusStream.listen((event) { sPort.send(['status', event]); diff --git a/packages/powersync/lib/src/database/powersync_database_impl_stub.dart b/packages/powersync/lib/src/database/powersync_database_impl_stub.dart index b41ec948..9dc96d4a 100644 --- a/packages/powersync/lib/src/database/powersync_database_impl_stub.dart +++ b/packages/powersync/lib/src/database/powersync_database_impl_stub.dart @@ -113,7 +113,9 @@ class PowerSyncDatabaseImpl @internal Future baseConnect( {required PowerSyncBackendConnector connector, - Duration crudThrottleTime = const Duration(milliseconds: 10)}) { + required Duration crudThrottleTime, + required Future Function() reconnect, + Map? params}) { throw UnimplementedError(); } } diff --git a/packages/powersync/lib/src/database/powersync_db_mixin.dart b/packages/powersync/lib/src/database/powersync_db_mixin.dart index 8ed77a49..b205e210 100644 --- a/packages/powersync/lib/src/database/powersync_db_mixin.dart +++ b/packages/powersync/lib/src/database/powersync_db_mixin.dart @@ -2,17 +2,15 @@ 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/database_utils.dart'; -import 'package:powersync/src/migrations.dart'; import 'package:powersync/src/powersync_update_notification.dart'; import 'package:powersync/src/schema.dart'; -import 'package:powersync/src/schema_helpers.dart'; +import 'package:powersync/src/schema_logic.dart'; import 'package:powersync/src/sync_status.dart'; -import 'package:sqlite_async/sqlite3_common.dart'; mixin PowerSyncDatabaseMixin implements SqliteConnection { /// Schema used for the local database. @@ -33,6 +31,8 @@ mixin PowerSyncDatabaseMixin implements SqliteConnection { /// 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); @@ -78,8 +78,9 @@ mixin PowerSyncDatabaseMixin implements SqliteConnection { .cast(); await database.initialize(); - await migrations.migrate(database); + await database.execute('SELECT powersync_init()'); await updateSchema(schema); + await _updateHasSynced(); } /// Wait for initialization to complete. @@ -89,11 +90,31 @@ mixin PowerSyncDatabaseMixin implements SqliteConnection { return isInitialized; } + Future _updateHasSynced() async { + const syncedSQL = + 'SELECT 1 FROM ps_buckets WHERE last_applied_op > 0 LIMIT 1'; + + // Query the database to see if any data has been synced. + final result = await database.execute(syncedSQL); + final hasSynced = result.rows.isNotEmpty; + + if (hasSynced != currentStatus.hasSynced) { + final status = SyncStatus(hasSynced: hasSynced); + setStatus(status); + } + } + @protected void setStatus(SyncStatus status) { if (status != currentStatus) { - currentStatus = status; - statusStreamController.add(status); + 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); + statusStreamController.add(currentStatus); } } @@ -128,9 +149,22 @@ mixin PowerSyncDatabaseMixin implements SqliteConnection { /// Throttle time between CRUD operations /// Defaults to 10 milliseconds. - Duration crudThrottleTime = const Duration(milliseconds: 10)}) async { - _connectMutex.lock(() => - baseConnect(connector: connector, crudThrottleTime: crudThrottleTime)); + 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 @@ -143,7 +177,9 @@ mixin PowerSyncDatabaseMixin implements SqliteConnection { /// Throttle time between CRUD operations /// Defaults to 10 milliseconds. - Duration crudThrottleTime = const Duration(milliseconds: 10)}); + required Duration crudThrottleTime, + required Future Function() reconnect, + Map? params}); /// Close the sync connection. /// @@ -347,29 +383,29 @@ mixin PowerSyncDatabaseMixin implements SqliteConnection { {String? debugContext, Duration? lockTimeout}); @override - Future writeTransaction( - Future Function(SqliteWriteContext tx) callback, - {Duration? lockTimeout, - String? debugContext}) async { - return writeLock((ctx) async { - return await internalTrackedWriteTransaction(ctx, callback); - }, - lockTimeout: lockTimeout, - debugContext: debugContext ?? 'writeTransaction()'); + 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); } - @override - Future execute(String sql, - [List parameters = const []]) async { - return writeLock((ctx) async { - try { - await ctx.execute( - 'UPDATE ps_tx SET current_tx = next_tx, next_tx = next_tx + 1 WHERE id = 1'); - return await ctx.execute(sql, parameters); - } finally { - await ctx.execute('UPDATE ps_tx SET current_tx = NULL WHERE id = 1'); - } - }, debugContext: 'execute()'); + @protected + String _prefixTableNames(String tableName, String prefix) { + String prefixedString = tableName.replaceRange(0, 0, prefix); + return prefixedString; } @override diff --git a/packages/powersync/lib/src/database/web/web_powersync_database.dart b/packages/powersync/lib/src/database/web/web_powersync_database.dart index 60dfcb26..5142fa80 100644 --- a/packages/powersync/lib/src/database/web/web_powersync_database.dart +++ b/packages/powersync/lib/src/database/web/web_powersync_database.dart @@ -7,14 +7,13 @@ 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/database_utils.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:sqlite_async/sqlite_async.dart'; -import 'package:powersync/src/schema_helpers.dart' as schema_helpers; +import 'package:powersync/src/schema_logic.dart' as schema_logic; /// A PowerSync managed database. /// @@ -125,7 +124,9 @@ class PowerSyncDatabaseImpl /// Throttle time between CRUD operations /// Defaults to 10 milliseconds. - Duration crudThrottleTime = const Duration(milliseconds: 10)}) async { + required Duration crudThrottleTime, + required Future Function() reconnect, + Map? params}) async { await initialize(); // Disconnect if connected @@ -144,13 +145,18 @@ class PowerSyncDatabaseImpl updateStream: updates, retryDelay: Duration(seconds: 3), client: FetchClient(mode: RequestMode.cors), + syncParameters: params, // 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); sync.statusStream.listen((event) { setStatus(event); }); - sync.streamingSync(abortController: disconnecter); + sync.streamingSync(); + disconnecter?.onAbort.then((_) async { + await sync.abort(); + disconnecter?.completeAbort(); + }).ignore(); } /// Takes a read lock, without starting a transaction. @@ -184,19 +190,16 @@ class PowerSyncDatabaseImpl debugContext: debugContext, lockTimeout: lockTimeout); } - @override - /// Uses the database writeTransaction instead of the locally /// scoped writeLock. This is to allow the Database transaction /// tracking to be correctly configured. + @override Future writeTransaction( Future Function(SqliteWriteContext tx) callback, {Duration? lockTimeout, String? debugContext}) async { await isInitialized; - return database.writeTransaction( - (context) => internalTrackedWrite(context, callback), - lockTimeout: lockTimeout); + return database.writeTransaction(callback, lockTimeout: lockTimeout); } @override @@ -205,6 +208,6 @@ class PowerSyncDatabaseImpl throw AssertionError('Cannot update schema while connected'); } this.schema = schema; - return database.writeLock((tx) => schema_helpers.updateSchema(tx, schema)); + return database.writeLock((tx) => schema_logic.updateSchema(tx, schema)); } } diff --git a/packages/powersync/lib/src/database_utils.dart b/packages/powersync/lib/src/database_utils.dart deleted file mode 100644 index 26369cc1..00000000 --- a/packages/powersync/lib/src/database_utils.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'dart:async'; - -import 'package:sqlite_async/sqlite_async.dart'; -import 'package:sqlite_async/sqlite3_common.dart' as sqlite; - -Future asyncDirectTransaction(sqlite.CommonDatabase db, - FutureOr Function(sqlite.CommonDatabase db) callback) async { - for (var i = 50; i >= 0; i--) { - try { - db.execute('BEGIN IMMEDIATE'); - late T result; - try { - result = await callback(db); - db.execute('COMMIT'); - } catch (e) { - try { - db.execute('ROLLBACK'); - } catch (e2) { - // Safe to ignore - } - rethrow; - } - - return result; - } catch (e) { - if (e is sqlite.SqliteException) { - if (e.resultCode == 5 && i != 0) { - // SQLITE_BUSY - await Future.delayed(const Duration(milliseconds: 50)); - continue; - } - } - rethrow; - } - } - throw AssertionError('Should not reach this'); -} - -Future internalTrackedWriteTransaction(SqliteWriteContext ctx, - Future Function(SqliteWriteContext tx) callback) async { - try { - await ctx.execute('BEGIN IMMEDIATE'); - final result = await internalTrackedWrite(ctx, callback); - await ctx.execute('COMMIT'); - return result; - } catch (e) { - try { - await ctx.execute('ROLLBACK'); - } catch (e) { - // In rare cases, a ROLLBACK may fail. - // Safe to ignore. - } - rethrow; - } -} - -/// Internally tracks a write -/// The transaction is assumed to be started externally -Future internalTrackedWrite(SqliteWriteContext ctx, - Future Function(SqliteWriteContext tx) callback) async { - await ctx.execute( - 'UPDATE ps_tx SET current_tx = next_tx, next_tx = next_tx + 1 WHERE id = 1'); - final result = await callback(ctx); - await ctx.execute('UPDATE ps_tx SET current_tx = NULL WHERE id = 1'); - return result; -} diff --git a/packages/powersync/lib/src/exceptions.dart b/packages/powersync/lib/src/exceptions.dart index ecbe39d7..d340dd60 100644 --- a/packages/powersync/lib/src/exceptions.dart +++ b/packages/powersync/lib/src/exceptions.dart @@ -88,3 +88,10 @@ String? _stringOrFirst(Object? details) { return null; } } + +class PowersyncNotReadyException implements Exception { + /// @nodoc + PowersyncNotReadyException(this.message); + + final String message; +} diff --git a/packages/powersync/lib/src/migrations.dart b/packages/powersync/lib/src/migrations.dart deleted file mode 100644 index 47320852..00000000 --- a/packages/powersync/lib/src/migrations.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:sqlite_async/sqlite_async.dart'; - -final migrations = SqliteMigrations(migrationTable: 'ps_migration') - ..add(SqliteMigration(1, (tx) async { - final List dropCommands = [ - 'DROP TABLE IF EXISTS crud;', - 'DROP TABLE IF EXISTS oplog;', - 'DROP TABLE IF EXISTS buckets;', - 'DROP TABLE IF EXISTS objects_untyped;', - 'DROP TABLE IF EXISTS ps_oplog;', - 'DROP TABLE IF EXISTS ps_buckets;', - 'DROP TABLE IF EXISTS ps_untyped;', - 'DROP TABLE IF EXISTS ps_migrations;' - ]; - - for (var row in dropCommands) { - await tx.execute(row); - } - - final existingTableRows = await tx.execute( - "SELECT name FROM sqlite_master WHERE type='table' AND name GLOB 'objects__*'"); - - for (var row in existingTableRows) { - await tx.execute('DROP TABLE ${row['name']}'); - } - - final List opLogCommands = [ - '''CREATE TABLE ps_oplog( - bucket TEXT NOT NULL, - op_id INTEGER NOT NULL, - op INTEGER NOT NULL, - row_type TEXT, - row_id TEXT, - key TEXT, - data TEXT, - hash INTEGER NOT NULL, - superseded INTEGER NOT NULL);''', - '''CREATE INDEX ps_oplog_by_row ON ps_oplog (row_type, row_id) WHERE superseded = 0;''', - '''CREATE INDEX ps_oplog_by_opid ON ps_oplog (bucket, op_id);''', - '''CREATE INDEX ps_oplog_by_key ON ps_oplog (bucket, key) WHERE superseded = 0;''', - '''CREATE TABLE ps_buckets( - name TEXT PRIMARY KEY, - last_applied_op INTEGER NOT NULL DEFAULT 0, - last_op INTEGER NOT NULL DEFAULT 0, - target_op INTEGER NOT NULL DEFAULT 0, - add_checksum INTEGER NOT NULL DEFAULT 0, - pending_delete INTEGER NOT NULL DEFAULT 0 - );''', - '''CREATE TABLE ps_untyped(type TEXT NOT NULL, id TEXT NOT NULL, data TEXT, PRIMARY KEY (type, id));''', - '''CREATE TABLE IF NOT EXISTS ps_crud (id INTEGER PRIMARY KEY AUTOINCREMENT, data TEXT);''' - ]; - - for (var row in opLogCommands) { - await tx.execute(row); - } - })) - ..add(SqliteMigration(2, (tx) async { - final List opLogCommands = [ - 'CREATE TABLE ps_tx(id INTEGER PRIMARY KEY NOT NULL, current_tx INTEGER, next_tx INTEGER);', - 'INSERT INTO ps_tx(id, current_tx, next_tx) VALUES(1, NULL, 1);', - 'ALTER TABLE ps_crud ADD COLUMN tx_id INTEGER;' - ]; - for (var row in opLogCommands) { - await tx.execute(row); - } - }, - downMigration: SqliteDownMigration(toVersion: 1) - ..add('DROP TABLE ps_tx') - ..add('ALTER TABLE ps_crud DROP COLUMN tx_id'))); diff --git a/packages/powersync/lib/src/open_factory/abstract_powersync_open_factory.dart b/packages/powersync/lib/src/open_factory/abstract_powersync_open_factory.dart index f89f2b80..06e8feda 100644 --- a/packages/powersync/lib/src/open_factory/abstract_powersync_open_factory.dart +++ b/packages/powersync/lib/src/open_factory/abstract_powersync_open_factory.dart @@ -66,6 +66,10 @@ abstract class AbstractPowerSyncOpenFactory extends DefaultSqliteOpenFactory { } throw AssertionError('Cannot reach this point'); } + + /// 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}); } /// Advanced: Define custom setup for each SQLite connection. diff --git a/packages/powersync/lib/src/open_factory/native/native_open_factory.dart b/packages/powersync/lib/src/open_factory/native/native_open_factory.dart index 59416291..9f0d3713 100644 --- a/packages/powersync/lib/src/open_factory/native/native_open_factory.dart +++ b/packages/powersync/lib/src/open_factory/native/native_open_factory.dart @@ -1,10 +1,12 @@ +import 'dart:ffi'; + +import 'package:powersync/powersync.dart'; import 'package:universal_io/io.dart'; import 'dart:isolate'; import 'package:powersync/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'; -import '../../uuid.dart'; /// Native implementation for [AbstractPowerSyncOpenFactory] class PowerSyncOpenFactory extends AbstractPowerSyncOpenFactory { @@ -20,24 +22,26 @@ class PowerSyncOpenFactory extends AbstractPowerSyncOpenFactory { : _sqliteSetup = sqliteSetup; @override - void enableExtension() {} + void enableExtension() { + var powersyncLib = _getDynamicLibraryForPlatform(); + sqlite.sqlite3.ensureExtensionLoaded(sqlite.SqliteExtension.inLibrary( + powersyncLib, 'sqlite3_powersync_init')); + } + + /// Returns the dynamic library for the current platform. + DynamicLibrary _getDynamicLibraryForPlatform() { + /// When running tests, we need to load the library for all platforms. + if (Platform.environment.containsKey('FLUTTER_TEST')) { + return DynamicLibrary.open(getLibraryForPlatform()); + } + return (Platform.isIOS || Platform.isMacOS) + ? DynamicLibrary.process() + : DynamicLibrary.open(getLibraryForPlatform()); + } @override setupFunctions(CommonDatabase db) { super.setupFunctions(db); - db.createFunction( - functionName: 'uuid', - argumentCount: const AllowedArgumentCount(0), - function: (args) { - return uuid.v4(); - }, - ); - db.createFunction( - // Postgres compatibility - functionName: 'gen_random_uuid', - argumentCount: const AllowedArgumentCount(0), - function: (args) => uuid.v4(), - ); db.createFunction( functionName: 'powersync_sleep', argumentCount: const sqlite.AllowedArgumentCount(1), @@ -61,8 +65,40 @@ class PowerSyncOpenFactory extends AbstractPowerSyncOpenFactory { CommonDatabase open(SqliteOpenOptions options) { // ignore: deprecated_member_use_from_same_package _sqliteSetup?.setup(); + + enableExtension(); + var db = super.open(options); db.execute('PRAGMA recursive_triggers = TRUE'); return db; } + + @override + String getLibraryForPlatform({String? path}) { + switch (Abi.current()) { + case Abi.androidArm: + case Abi.androidArm64: + case Abi.androidX64: + return 'libpowersync.so'; + case Abi.macosArm64: + case Abi.macosX64: + return 'libpowersync.dylib'; + case Abi.linuxX64: + return 'libpowersync.so'; + case Abi.windowsArm64: + case Abi.windowsX64: + return '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.', + ); + } + } } diff --git a/packages/powersync/lib/src/open_factory/open_factory_stub.dart b/packages/powersync/lib/src/open_factory/open_factory_stub.dart index aa571b0f..182cf895 100644 --- a/packages/powersync/lib/src/open_factory/open_factory_stub.dart +++ b/packages/powersync/lib/src/open_factory/open_factory_stub.dart @@ -18,4 +18,9 @@ class PowerSyncOpenFactory extends open_factory.AbstractPowerSyncOpenFactory { void setupFunctions(CommonDatabase db) { throw UnimplementedError(); } + + @override + String getLibraryForPlatform({String? path}) { + throw UnimplementedError(); + } } diff --git a/packages/powersync/lib/src/open_factory/web/web_open_factory.dart b/packages/powersync/lib/src/open_factory/web/web_open_factory.dart index eaf06c1f..8c137393 100644 --- a/packages/powersync/lib/src/open_factory/web/web_open_factory.dart +++ b/packages/powersync/lib/src/open_factory/web/web_open_factory.dart @@ -48,4 +48,10 @@ class PowerSyncOpenFactory extends AbstractPowerSyncOpenFactory { function: (args) => uuid.v4(), ); } + + @override + String getLibraryForPlatform({String? path}) { + // no op for web + return ""; + } } diff --git a/packages/powersync/lib/src/powersync_update_notification.dart b/packages/powersync/lib/src/powersync_update_notification.dart index 0f09e374..b97a27ce 100644 --- a/packages/powersync/lib/src/powersync_update_notification.dart +++ b/packages/powersync/lib/src/powersync_update_notification.dart @@ -1,5 +1,5 @@ import 'package:sqlite_async/sqlite_async.dart'; -import 'schema_helpers.dart'; +import 'schema_logic.dart'; class PowerSyncUpdateNotification extends UpdateNotification { PowerSyncUpdateNotification(super.tables); diff --git a/packages/powersync/lib/src/schema.dart b/packages/powersync/lib/src/schema.dart index 8993b30d..8d91c32f 100644 --- a/packages/powersync/lib/src/schema.dart +++ b/packages/powersync/lib/src/schema.dart @@ -1,4 +1,4 @@ -import './schema_helpers.dart'; +import 'schema_logic.dart'; /// The schema used by the database. /// @@ -9,6 +9,8 @@ class Schema { final List tables; const Schema(this.tables); + + Map toJson() => {'tables': tables}; } /// A single table in the schema. @@ -130,6 +132,15 @@ class Table { 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 { @@ -155,13 +166,10 @@ class Index { return "${table.internalName}__$name"; } - /// Internal use only. - /// - /// Returns a SQL statement that creates this index. - String toSqlDefinition(Table table) { - var fields = columns.map((column) => column.toSql(table)).join(', '); - return 'CREATE INDEX "${fullName(table)}" ON "${table.internalName}"($fields)'; - } + Map toJson(Table table) => { + 'name': name, + 'columns': columns.map((c) => c.toJson(table)).toList(growable: false) + }; } /// Describes an indexed column. @@ -176,14 +184,10 @@ class IndexedColumn { const IndexedColumn.ascending(this.column) : ascending = true; const IndexedColumn.descending(this.column) : ascending = false; - String toSql(Table table) { - final fullColumn = table[column]; // errors if not found + Map toJson(Table table) { + final t = table[column].type; - if (ascending) { - return mapColumn(fullColumn); - } else { - return "${mapColumn(fullColumn)} DESC"; - } + return {'name': column, 'ascending': ascending, 'type': t.sqlite}; } } @@ -211,6 +215,8 @@ class Column { /// Create a REAL column. const Column.real(this.name) : type = ColumnType.real; + + Map toJson() => {'name': name, 'type': type.sqlite}; } /// Type of column. diff --git a/packages/powersync/lib/src/schema_helpers.dart b/packages/powersync/lib/src/schema_helpers.dart deleted file mode 100644 index 72d95d9e..00000000 --- a/packages/powersync/lib/src/schema_helpers.dart +++ /dev/null @@ -1,318 +0,0 @@ -import 'package:powersync/sqlite_async.dart'; - -import 'schema.dart'; - -const String maxOpId = '9223372036854775807'; - -final invalidSqliteCharacters = RegExp(r'''["'%,\.#\s\[\]]'''); - -/// Since view names don't have a static prefix, mark views as auto-generated by adding a comment. -final _autoGenerated = '-- powersync-auto-generated'; - -String createViewStatement(Table table) { - final columnNames = - table.columns.map((column) => quoteIdentifier(column.name)).join(', '); - - if (table.insertOnly) { - final nulls = table.columns.map((column) => 'NULL').join(', '); - return 'CREATE VIEW ${quoteIdentifier(table.viewName)}("id", $columnNames) AS SELECT NULL, $nulls WHERE 0 $_autoGenerated'; - } - final select = table.columns.map(mapColumn).join(', '); - return 'CREATE VIEW ${quoteIdentifier(table.viewName)}("id", $columnNames) AS SELECT "id", $select FROM ${quoteIdentifier(table.internalName)} $_autoGenerated'; -} - -String mapColumn(Column column) { - return "CAST(json_extract(data, ${quoteJsonPath(column.name)}) as ${column.type.sqlite})"; -} - -List createViewTriggerStatements(Table table) { - if (table.localOnly) { - return createViewTriggerStatementsLocal(table); - } else if (table.insertOnly) { - return createViewTriggerStatementsInsert(table); - } - final viewName = table.viewName; - final type = table.name; - final internalNameE = quoteIdentifier(table.internalName); - - final jsonFragment = table.columns - .map((column) => - "${quoteString(column.name)}, NEW.${quoteIdentifier(column.name)}") - .join(', '); - final jsonFragmentOld = table.columns - .map((column) => - "${quoteString(column.name)}, OLD.${quoteIdentifier(column.name)}") - .join(', '); - // Names in alphabetical order - return [ - """ -CREATE TRIGGER ${quoteIdentifier('ps_view_delete_$viewName')} -INSTEAD OF DELETE ON ${quoteIdentifier(viewName)} -FOR EACH ROW -BEGIN - DELETE FROM $internalNameE WHERE id = OLD.id; - INSERT INTO ps_crud(tx_id, data) SELECT current_tx, json_object('op', 'DELETE', 'type', ${quoteString(type)}, 'id', OLD.id) FROM ps_tx WHERE id = 1; - INSERT INTO ps_oplog(bucket, op_id, op, row_type, row_id, hash, superseded) - SELECT '\$local', - 1, - 'REMOVE', - ${quoteString(type)}, - OLD.id, - 0, - 0; - INSERT OR REPLACE INTO ps_buckets(name, pending_delete, last_op, target_op) VALUES('\$local', 1, 0, $maxOpId); -END""", - """ -CREATE TRIGGER ${quoteIdentifier('ps_view_insert_$viewName')} -INSTEAD OF INSERT ON ${quoteIdentifier(viewName)} -FOR EACH ROW -BEGIN - SELECT CASE - WHEN (NEW.id IS NULL) - THEN RAISE (FAIL, 'id is required') - END; - INSERT INTO $internalNameE(id, data) - SELECT NEW.id, json_object($jsonFragment); - INSERT INTO ps_crud(tx_id, data) SELECT current_tx, json_object('op', 'PUT', 'type', ${quoteString(type)}, 'id', NEW.id, 'data', json(powersync_diff('{}', json_object($jsonFragment)))) FROM ps_tx WHERE id = 1; - INSERT INTO ps_oplog(bucket, op_id, op, row_type, row_id, hash, superseded) - SELECT '\$local', - 1, - 'REMOVE', - ${quoteString(type)}, - NEW.id, - 0, - 0; - INSERT OR REPLACE INTO ps_buckets(name, pending_delete, last_op, target_op) VALUES('\$local', 1, 0, $maxOpId); -END""", - """ -CREATE TRIGGER ${quoteIdentifier('ps_view_update_$viewName')} -INSTEAD OF UPDATE ON ${quoteIdentifier(viewName)} -FOR EACH ROW -BEGIN - SELECT CASE - WHEN (OLD.id != NEW.id) - THEN RAISE (FAIL, 'Cannot update id') - END; - UPDATE $internalNameE - SET data = json_object($jsonFragment) - WHERE id = NEW.id; - INSERT INTO ps_crud(tx_id, data) SELECT current_tx, json_object('op', 'PATCH', 'type', ${quoteString(type)}, 'id', NEW.id, 'data', json(powersync_diff(json_object($jsonFragmentOld), json_object($jsonFragment)))) FROM ps_tx WHERE id = 1; - INSERT INTO ps_oplog(bucket, op_id, op, row_type, row_id, hash, superseded) - SELECT '\$local', - 1, - 'REMOVE', - ${quoteString(type)}, - NEW.id, - 0, - 0; - INSERT OR REPLACE INTO ps_buckets(name, pending_delete, last_op, target_op) VALUES('\$local', 1, 0, $maxOpId); -END""" - ]; -} - -List createViewTriggerStatementsLocal(Table table) { - final viewName = table.viewName; - final internalNameE = quoteIdentifier(table.internalName); - - final jsonFragment = table.columns - .map((column) => - "${quoteString(column.name)}, NEW.${quoteIdentifier(column.name)}") - .join(', '); - // Names in alphabetical order - return [ - """ -CREATE TRIGGER ${quoteIdentifier('ps_view_delete_$viewName')} -INSTEAD OF DELETE ON ${quoteIdentifier(viewName)} -FOR EACH ROW -BEGIN - DELETE FROM $internalNameE WHERE id = OLD.id; -END""", - """ -CREATE TRIGGER ${quoteIdentifier('ps_view_insert_$viewName')} -INSTEAD OF INSERT ON ${quoteIdentifier(viewName)} -FOR EACH ROW -BEGIN - INSERT INTO $internalNameE(id, data) - SELECT NEW.id, json_object($jsonFragment); -END""", - """ -CREATE TRIGGER ${quoteIdentifier('ps_view_update_$viewName')} -INSTEAD OF UPDATE ON ${quoteIdentifier(viewName)} -FOR EACH ROW -BEGIN - SELECT CASE - WHEN (OLD.id != NEW.id) - THEN RAISE (FAIL, 'Cannot update id') - END; - UPDATE $internalNameE - SET data = json_object($jsonFragment) - WHERE id = NEW.id; -END""" - ]; -} - -List createViewTriggerStatementsInsert(Table table) { - final type = table.name; - final viewName = table.viewName; - - final jsonFragment = table.columns - .map((column) => - "${quoteString(column.name)}, NEW.${quoteIdentifier(column.name)}") - .join(', '); - return [ - """ -CREATE TRIGGER ${quoteIdentifier('ps_view_insert_$viewName')} -INSTEAD OF INSERT ON ${quoteIdentifier(viewName)} -FOR EACH ROW -BEGIN - INSERT INTO ps_crud(tx_id, data) SELECT current_tx, json_object('op', 'PUT', 'type', ${quoteString(type)}, 'id', NEW.id, 'data', json(powersync_diff('{}', json_object($jsonFragment)))) FROM ps_tx WHERE id = 1; -END""" - ]; -} - -/// Sync the schema to the local database. -/// Must be wrapped in a transaction. -Future updateSchema(SqliteWriteContext tx, Schema schema) async { - for (var table in schema.tables) { - table.validate(); - } - - await _createTablesAndIndexes(tx, schema); - - final existingViewRows = await tx.execute( - "SELECT name FROM sqlite_master WHERE type='view' AND sql GLOB '*$_autoGenerated'"); - - Set toRemove = {for (var row in existingViewRows) row['name']}; - - for (var table in schema.tables) { - toRemove.remove(table.viewName); - - var createViewOp = createViewStatement(table); - var triggers = createViewTriggerStatements(table); - var existingRows = await tx.execute( - "SELECT sql FROM sqlite_master WHERE (type = 'view' AND name = ?) OR (type = 'trigger' AND tbl_name = ?) ORDER BY type DESC, name ASC", - [table.viewName, table.viewName]); - if (existingRows.isNotEmpty) { - final dbSql = existingRows.map((row) => row['sql']).join('\n\n'); - final generatedSql = - [createViewOp, for (var trigger in triggers) trigger].join('\n\n'); - if (dbSql == generatedSql) { - // No change - keep it. - continue; - } else { - // View and/or triggers changed - delete and re-create. - await tx.execute('DROP VIEW ${quoteIdentifier(table.viewName)}'); - } - } else { - // New - create - } - await tx.execute(createViewOp); - for (final op in triggers) { - await tx.execute(op); - } - } - - for (var name in toRemove) { - await tx.execute('DROP VIEW ${quoteIdentifier(name)}'); - } -} - -/// Sync the schema to the local database. -/// -/// Does not create triggers or temporary views. -/// -/// Must be wrapped in a transaction. -Future _createTablesAndIndexes( - SqliteWriteContext tx, Schema schema) async { - // Make sure to refresh tables in the same transaction as updating them - final existingTableRows = await tx.execute( - "SELECT name FROM sqlite_master WHERE type='table' AND name GLOB 'ps_data_*'"); - final existingIndexRows = await tx.execute( - "SELECT name, sql FROM sqlite_master WHERE type='index' AND name GLOB 'ps_data_*'"); - - final Set remainingTables = {}; - final Map indexesToDrop = {}; - final List createIndexes = []; - for (final row in existingTableRows) { - remainingTables.add(row['name'] as String); - } - for (final row in existingIndexRows) { - indexesToDrop[row['name'] as String] = row['sql'] as String; - } - - for (final table in schema.tables) { - for (final index in table.indexes) { - final fullName = index.fullName(table); - final sql = index.toSqlDefinition(table); - if (indexesToDrop.containsKey(fullName)) { - final existingSql = indexesToDrop[fullName]; - if (existingSql == sql) { - // No change (don't drop) - indexesToDrop.remove(fullName); - } else { - // Drop and create - createIndexes.add(sql); - } - } else { - // New index - create - createIndexes.add(sql); - } - } - } - - for (final table in schema.tables) { - if (table.insertOnly) { - // Does not have a physical table - continue; - } - final tableName = table.internalName; - final exists = remainingTables.contains(tableName); - remainingTables.remove(tableName); - if (exists) { - continue; - } - - await tx.execute("""CREATE TABLE ${quoteIdentifier(tableName)} - ( - id TEXT PRIMARY KEY NOT NULL, - data TEXT - )"""); - - if (!table.localOnly) { - await tx.execute("""INSERT INTO ${quoteIdentifier(tableName)}(id, data) - SELECT id, data - FROM ps_untyped - WHERE type = ?""", [table.name]); - await tx.execute("""DELETE - FROM ps_untyped - WHERE type = ?""", [table.name]); - } - } - - for (final indexName in indexesToDrop.keys) { - await tx.execute('DROP INDEX ${quoteIdentifier(indexName)}'); - } - - for (final sql in createIndexes) { - await tx.execute(sql); - } - - for (final tableName in remainingTables) { - final typeMatch = RegExp("^ps_data__(.+)\$").firstMatch(tableName); - if (typeMatch != null) { - // Not local-only - final type = typeMatch[1]; - await tx.execute( - 'INSERT INTO ps_untyped(type, id, data) SELECT ?, id, data FROM ${quoteIdentifier(tableName)}', - [type]); - } - await tx.execute('DROP TABLE ${quoteIdentifier(tableName)}'); - } -} - -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); -} diff --git a/packages/powersync/lib/src/schema_logic.dart b/packages/powersync/lib/src/schema_logic.dart index 3066d98c..93296c5a 100644 --- a/packages/powersync/lib/src/schema_logic.dart +++ b/packages/powersync/lib/src/schema_logic.dart @@ -1,7 +1,17 @@ -import 'package:powersync/sqlite_async.dart'; +import 'dart:convert'; + +import 'package:sqlite_async/sqlite_async.dart'; import 'schema.dart'; -import 'schema_helpers.dart'; + +const String maxOpId = '9223372036854775807'; + +final invalidSqliteCharacters = RegExp(r'''["'%,\.#\s\[\]]'''); + +/// Sync the schema to the local database. +Future updateSchema(SqliteWriteContext tx, Schema schema) async { + await tx.execute('SELECT powersync_replace_schema(?)', [jsonEncode(schema)]); +} Future updateSchemaInIsolate( SqliteConnection database, Schema schema) async { @@ -9,3 +19,10 @@ Future updateSchemaInIsolate( await updateSchema(tx, schema); }); } + +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); +} diff --git a/packages/powersync/lib/src/streaming_sync.dart b/packages/powersync/lib/src/streaming_sync.dart index ff925588..28f77c53 100644 --- a/packages/powersync/lib/src/streaming_sync.dart +++ b/packages/powersync/lib/src/streaming_sync.dart @@ -32,12 +32,19 @@ class StreamingSyncImplementation { late final http.Client _client; - final StreamController _localPingController = StreamController.broadcast(); + final StreamController _localPingController = + StreamController.broadcast(); final Duration retryDelay; + final Map? syncParameters; + SyncStatus lastStatus = const SyncStatus(); + AbortController? _abort; + + bool _safeToClose = true; + final Mutex syncMutex, crudMutex; StreamingSyncImplementation( @@ -47,6 +54,7 @@ class StreamingSyncImplementation { required this.uploadCrud, required this.updateStream, required this.retryDelay, + this.syncParameters, required http.Client client, /// A unique identifier for this streaming sync implementation @@ -58,40 +66,76 @@ class StreamingSyncImplementation { statusStream = _statusStreamController.stream; } - Future streamingSync({AbortController? abortController}) async { - crudLoop(); - var invalidCredentials = false; - while (true) { - if (abortController?.aborted == true) { - abortController!.completeAbort(); - return; - } - _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: abortController), - timeout: retryDelay); - } catch (e, stacktrace) { - final message = _syncErrorMessage(e); - isolateLogger.warning('Sync error: $message', e, stacktrace); - invalidCredentials = true; + /// Close any active streams. + 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(); + } + // wait for completeAbort() to be called + await future; - _updateStatus( - connected: false, - connecting: true, - downloading: false, - downloadError: e); + // Now close the client in all cases not covered above + _client.close(); + } - // On error, wait a little before retrying - await Future.delayed(retryDelay); + bool get aborted { + return _abort?.aborted ?? false; + } + + Future streamingSync() async { + try { + _abort = AbortController(); + 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 Future.any([Future.delayed(retryDelay), _abort!.onAbort]); + } } + } finally { + _abort!.completeAbort(); } } @@ -168,6 +212,7 @@ class StreamingSyncImplementation { /// To clear errors, use [_noError] instead of null. void _updateStatus( {DateTime? lastSyncedAt, + bool? hasSynced, bool? connected, bool? connecting, bool? downloading, @@ -179,6 +224,7 @@ class StreamingSyncImplementation { 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 @@ -202,9 +248,9 @@ class StreamingSyncImplementation { initialBucketStates[entry.bucket] = entry.opId; } - final List req = []; + final List buckets = []; for (var entry in initialBucketStates.entries) { - req.add(BucketRequest(entry.key, entry.value)); + buckets.add(BucketRequest(entry.key, entry.value)); } Checkpoint? targetCheckpoint; @@ -212,7 +258,8 @@ class StreamingSyncImplementation { Checkpoint? appliedCheckpoint; var bucketSet = Set.from(initialBucketStates.keys); - var requestStream = streamingSyncRequest(StreamingSyncRequest(req)); + var requestStream = + streamingSyncRequest(StreamingSyncRequest(buckets, syncParameters)); var merged = addBroadcast(requestStream, _localPingController.stream); @@ -220,9 +267,10 @@ class StreamingSyncImplementation { bool haveInvalidated = false; await for (var line in merged) { - if (abortController?.aborted == true) { - return false; + if (aborted) { + break; } + _updateStatus(connected: true, connecting: false); if (line is Checkpoint) { targetCheckpoint = line; @@ -234,7 +282,6 @@ class StreamingSyncImplementation { } bucketSet = newBuckets; await adapter.removeBuckets([...bucketsToDelete]); - adapter.setTargetCheckpoint(targetCheckpoint); _updateStatus(downloading: true); } else if (line is StreamingSyncCheckpointComplete) { final result = await adapter.syncLocalDatabase(targetCheckpoint!); @@ -262,6 +309,7 @@ class StreamingSyncImplementation { throw PowerSyncProtocolException( 'Checkpoint diff without previous checkpoint'); } + _updateStatus(downloading: true); final diff = line; final Map newBuckets = {}; for (var checksum in targetCheckpoint.checksums) { @@ -283,10 +331,9 @@ class StreamingSyncImplementation { bucketSet = Set.from(newBuckets.keys); await adapter.removeBuckets(diff.removedBuckets); adapter.setTargetCheckpoint(targetCheckpoint); - _updateStatus(downloading: true); } else if (line is SyncBucketData) { - await adapter.saveSyncData(SyncDataBatch([line])); _updateStatus(downloading: true); + await adapter.saveSyncData(SyncDataBatch([line])); } else if (line is StreamingSyncKeepalive) { if (line.tokenExpiresIn == 0) { // Token expired already - stop the connection immediately @@ -326,6 +373,7 @@ class StreamingSyncImplementation { // Continue waiting. } else { appliedCheckpoint = targetCheckpoint; + _updateStatus( downloading: false, downloadError: _noError, @@ -354,7 +402,17 @@ class StreamingSyncImplementation { request.headers['Authorization'] = "Token ${credentials.token}"; request.body = convert.jsonEncode(data); - final res = await _client.send(request); + 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) { @@ -367,6 +425,9 @@ class StreamingSyncImplementation { // 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); } } diff --git a/packages/powersync/lib/src/sync_status.dart b/packages/powersync/lib/src/sync_status.dart index 4ad7152c..a1f4a543 100644 --- a/packages/powersync/lib/src/sync_status.dart +++ b/packages/powersync/lib/src/sync_status.dart @@ -24,6 +24,10 @@ class SyncStatus { /// Currently this is reset to null after a restart. 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. @@ -38,6 +42,7 @@ class SyncStatus { {this.connected = false, this.connecting = false, this.lastSyncedAt, + this.hasSynced, this.downloading = false, this.uploading = false, this.downloadError, @@ -52,7 +57,30 @@ class SyncStatus { other.connecting == connecting && other.downloadError == downloadError && other.uploadError == uploadError && - other.lastSyncedAt == lastSyncedAt); + 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]. @@ -68,7 +96,7 @@ class SyncStatus { @override String toString() { - return "SyncStatus"; + return "SyncStatus"; } } diff --git a/packages/powersync/lib/src/sync_types.dart b/packages/powersync/lib/src/sync_types.dart index a9591909..627d0987 100644 --- a/packages/powersync/lib/src/sync_types.dart +++ b/packages/powersync/lib/src/sync_types.dart @@ -14,6 +14,16 @@ class 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 { @@ -102,15 +112,23 @@ Object? parseStreamingSyncLine(Map line) { class StreamingSyncRequest { List buckets; bool includeChecksum = true; + Map? parameters; - StreamingSyncRequest(this.buckets); + StreamingSyncRequest(this.buckets, this.parameters); - Map toJson() => { - 'buckets': buckets, - 'include_checksum': includeChecksum, - // We want the JSON row data as a string - 'raw_data': true - }; + Map toJson() { + final Map json = { + 'buckets': buckets, + 'include_checksum': includeChecksum, + 'raw_data': true, + }; + + if (parameters != null) { + json['parameters'] = parameters; + } + + return json; + } } class BucketRequest { diff --git a/packages/powersync/pubspec.yaml b/packages/powersync/pubspec.yaml index 87a9cb0d..e3fd67de 100644 --- a/packages/powersync/pubspec.yaml +++ b/packages/powersync/pubspec.yaml @@ -1,10 +1,10 @@ name: powersync -version: 1.3.0-alpha.9 +version: 1.5.5 homepage: https://powersync.com repository: https://github.com/powersync-ja/powersync.dart description: PowerSync Flutter SDK - keep PostgreSQL databases in sync with on-device SQLite databases. environment: - sdk: ">=3.3.0 <4.0.0" + sdk: ^3.4.0 dependencies: # Needed because of sqlite3_flutter_libs flutter: @@ -13,7 +13,8 @@ dependencies: sqlite_async: ^0.8.1 universal_io: ^2.0.0 - sqlite3_flutter_libs: ^0.5.15 + sqlite3_flutter_libs: ^0.5.23 + powersync_flutter_libs: ^0.1.0 meta: ^1.0.0 http: ^1.1.0 uuid: ^4.2.0 @@ -21,7 +22,7 @@ dependencies: logging: ^1.1.1 collection: ^1.17.0 fetch_client: ^1.1.2 - js: ^0.6.7 + js: ^0.7.0 dev_dependencies: dcli: ^4.0.0 lints: ^3.0.0 diff --git a/packages/powersync/test/bucket_storage_test.dart b/packages/powersync/test/bucket_storage_test.dart index b081fb52..6cf57613 100644 --- a/packages/powersync/test/bucket_storage_test.dart +++ b/packages/powersync/test/bucket_storage_test.dart @@ -1,7 +1,7 @@ import 'package:powersync/powersync.dart'; import 'package:powersync/src/bucket_storage.dart'; import 'package:powersync/src/sync_types.dart'; -import 'package:sqlite3/common.dart'; +import 'package:sqlite_async/sqlite3_common.dart'; import 'package:test/test.dart'; import 'utils/abstract_test_utils.dart'; @@ -51,7 +51,6 @@ void main() { powersync = await testUtils.setupPowerSync(path: path); bucketStorage = BucketStorage(powersync); - await bucketStorage.initialized(); }); tearDown(() async { diff --git a/packages/powersync/test/crud_test.dart b/packages/powersync/test/crud_test.dart index ebe036db..b01e71dc 100644 --- a/packages/powersync/test/crud_test.dart +++ b/packages/powersync/test/crud_test.dart @@ -96,11 +96,11 @@ void main() { ])); var tx = (await powersync.getNextCrudTransaction())!; - expect(tx.transactionId, equals(3)); + expect(tx.transactionId, equals(2)); expect( tx.crud, equals([ - CrudEntry(2, UpdateType.patch, 'assets', testId, 3, + CrudEntry(2, UpdateType.patch, 'assets', testId, 2, {"description": "test2"}) ])); }); @@ -120,9 +120,9 @@ void main() { ])); var tx = (await powersync.getNextCrudTransaction())!; - expect(tx.transactionId, equals(3)); + expect(tx.transactionId, equals(2)); expect(tx.crud, - equals([CrudEntry(2, UpdateType.delete, 'assets', testId, 3, null)])); + equals([CrudEntry(2, UpdateType.delete, 'assets', testId, 2, null)])); }); test('UPSERT not supported', () async { @@ -162,12 +162,11 @@ void main() { expect(await powersync.getAll('SELECT * FROM logs'), equals([])); var tx = (await powersync.getNextCrudTransaction())!; - - expect(tx.transactionId, equals(2)); + expect(tx.transactionId, equals(1)); expect( tx.crud, equals([ - CrudEntry(1, UpdateType.put, 'logs', testId, 2, + CrudEntry(1, UpdateType.put, 'logs', testId, 1, {"level": "INFO", "content": "test log"}) ])); }); diff --git a/packages/powersync/test/streaming_sync_test.dart b/packages/powersync/test/streaming_sync_test.dart index 8a395557..6d1e9e0d 100644 --- a/packages/powersync/test/streaming_sync_test.dart +++ b/packages/powersync/test/streaming_sync_test.dart @@ -63,6 +63,15 @@ void main() { 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(); } }); diff --git a/packages/powersync/test/test_server.dart b/packages/powersync/test/test_server.dart index 2b9ac8b6..8054582e 100644 --- a/packages/powersync/test/test_server.dart +++ b/packages/powersync/test/test_server.dart @@ -11,7 +11,6 @@ import 'package:shelf_router/shelf_router.dart'; class TestServer { late HttpServer server; Router app = Router(); - int connectionCount = 0; int maxConnectionCount = 0; int tokenExpiresIn; @@ -27,19 +26,22 @@ class TestServer { return 'http://${server.address.host}:${server.port}'; } + int get connectionCount { + return server.connectionsInfo().total; + } + + HttpConnectionsInfo connectionsInfo() { + return server.connectionsInfo(); + } + Future handleSyncStream(Request request) async { - connectionCount += 1; maxConnectionCount = max(connectionCount, maxConnectionCount); stream() async* { - try { - var blob = "*" * 5000; - for (var i = 0; i < 50; i++) { - yield {"token_expires_in": tokenExpiresIn, "blob": blob}; - await Future.delayed(Duration(microseconds: 1)); - } - } finally { - connectionCount -= 1; + var blob = "*" * 5000; + for (var i = 0; i < 50; i++) { + yield {"token_expires_in": tokenExpiresIn, "blob": blob}; + await Future.delayed(Duration(microseconds: 1)); } } diff --git a/packages/powersync/test/util.dart b/packages/powersync/test/util.dart new file mode 100644 index 00000000..a2bfc93a --- /dev/null +++ b/packages/powersync/test/util.dart @@ -0,0 +1,143 @@ +import 'dart:async'; +import 'dart:ffi'; +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:powersync/powersync.dart'; +import 'package:powersync/sqlite_async.dart'; +import 'package:sqlite3/open.dart' as sqlite_open; +import 'package:sqlite_async/sqlite3_common.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; + +class TestOpenFactory extends PowerSyncOpenFactory { + TestOpenFactory({required super.path}); + + @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 + 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: + return '$path/libpowersync.so'; + case Abi.windowsArm64: + 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 setupPowerSync( + {required String path, Schema? schema}) async { + final db = PowerSyncDatabase.withFactory(TestOpenFactory(path: path), + schema: schema ?? defaultSchema, logger: testLogger); + return db; +} + +Future setupSqlite( + {required PowerSyncDatabase powersync}) async { + await powersync.initialize(); + + final sqliteDb = await powersync.isolateConnectionFactory().openRawDatabase(); + + return sqliteDb; +} + +Future cleanDb({required String path}) async { + try { + await File(path).delete(); + } on PathNotFoundException { + // Not an issue + } + try { + await File("$path-shm").delete(); + } on PathNotFoundException { + // Not an issue + } + try { + await File("$path-wal").delete(); + } on PathNotFoundException { + // Not an issue + } +} + +String dbPath() { + final test = Invoker.current!.liveTest; + var testName = test.test.name; + var testShortName = testName.replaceAll(RegExp(r'\s'), '_').toLowerCase(); + var dbName = "test-db/$testShortName.db"; + Directory("test-db").createSync(recursive: false); + return dbName; +} + +final testLogger = _makeTestLogger(); + +Logger _makeTestLogger() { + final logger = Logger.detached('PowerSync Tests'); + logger.level = Level.ALL; + 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; +} diff --git a/packages/powersync/test/utils/abstract_test_utils.dart b/packages/powersync/test/utils/abstract_test_utils.dart index 59587573..eee5683f 100644 --- a/packages/powersync/test/utils/abstract_test_utils.dart +++ b/packages/powersync/test/utils/abstract_test_utils.dart @@ -1,6 +1,6 @@ import 'package:logging/logging.dart'; import 'package:powersync/powersync.dart'; -import 'package:sqlite3/common.dart'; +import 'package:sqlite_async/sqlite3_common.dart'; import 'package:sqlite_async/sqlite_async.dart'; import 'package:test_api/src/backend/invoker.dart'; diff --git a/packages/powersync/test/utils/native_test_utils.dart b/packages/powersync/test/utils/native_test_utils.dart index 00d3e429..353679e7 100644 --- a/packages/powersync/test/utils/native_test_utils.dart +++ b/packages/powersync/test/utils/native_test_utils.dart @@ -18,8 +18,40 @@ class TestOpenFactory extends PowerSyncOpenFactory { 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 + 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: + return '$path/libpowersync.so'; + case Abi.windowsArm64: + 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.', + ); + } + } } class TestUtils extends AbstractTestUtils { diff --git a/packages/powersync/test/utils/web_test_utils.dart b/packages/powersync/test/utils/web_test_utils.dart index a316fff3..41c43f7e 100644 --- a/packages/powersync/test/utils/web_test_utils.dart +++ b/packages/powersync/test/utils/web_test_utils.dart @@ -3,7 +3,7 @@ import 'dart:html'; import 'package:js/js.dart'; import 'package:powersync/powersync.dart'; -import 'package:sqlite3/src/database.dart'; +import 'package:sqlite_async/sqlite3_common.dart'; import 'package:sqlite_async/sqlite_async.dart'; import 'package:test/test.dart'; import 'abstract_test_utils.dart'; diff --git a/packages/powersync_attachments_helper/CHANGELOG.md b/packages/powersync_attachments_helper/CHANGELOG.md index 24a61a35..1472ba28 100644 --- a/packages/powersync_attachments_helper/CHANGELOG.md +++ b/packages/powersync_attachments_helper/CHANGELOG.md @@ -1,21 +1,30 @@ +## 0.5.1 + +- Upgrade `sqlite_async` to version 0.8.1. + +## 0.5.0 + +- Upgrade minimum Dart SDK constraint to `3.4.0`. +- Upgrade `sqlite_async` to version 0.7.0. + ## 0.3.0-alpha.4 - - Update a dependency to the latest release. +- Update a dependency to the latest release. ## 0.3.0-alpha.3 - - Update a dependency to the latest release. +- Update a dependency to the latest release. ## 0.3.0-alpha.2 > Note: This release has breaking changes. - - **FIX**: reset isProcessing when exception is thrown during sync process. (#81). - - **FIX**: attachment queue duplicating requests (#68). - - **FIX**(powersync-attachements-helper): pubspec file (#29). - - **FEAT**(attachments): add error handlers (#65). - - **DOCS**: update readmes (#38). - - **BREAKING** **FEAT**(attachments): cater for subdirectories in storage (#78). +- **FIX**: reset isProcessing when exception is thrown during sync process. (#81). +- **FIX**: attachment queue duplicating requests (#68). +- **FIX**(powersync-attachements-helper): pubspec file (#29). +- **FEAT**(attachments): add error handlers (#65). +- **DOCS**: update readmes (#38). +- **BREAKING** **FEAT**(attachments): cater for subdirectories in storage (#78). ## 0.4.1 diff --git a/packages/powersync_attachments_helper/pubspec.yaml b/packages/powersync_attachments_helper/pubspec.yaml index bfce3395..8eee1b69 100644 --- a/packages/powersync_attachments_helper/pubspec.yaml +++ b/packages/powersync_attachments_helper/pubspec.yaml @@ -1,18 +1,18 @@ name: powersync_attachments_helper description: A helper library for handling attachments when using PowerSync. -version: 0.3.0-alpha.4 +version: 0.5.1 repository: https://github.com/powersync-ja/powersync.dart homepage: https://www.powersync.com/ environment: - sdk: ">=3.3.0 <4.0.0" + sdk: ^3.4.0 dependencies: flutter: sdk: flutter - powersync: 1.3.0-alpha.9 + powersync: ^1.5.5 logging: ^1.2.0 - sqlite3: "^2.4.4" + sqlite_async: ^0.8.1 path_provider: ^2.0.13 dev_dependencies: diff --git a/packages/powersync_flutter_libs/.gitignore b/packages/powersync_flutter_libs/.gitignore new file mode 100644 index 00000000..1593a1f1 --- /dev/null +++ b/packages/powersync_flutter_libs/.gitignore @@ -0,0 +1,34 @@ +# 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/ + +*.so +*.a +*.dylib +*.dll diff --git a/packages/powersync_flutter_libs/.metadata b/packages/powersync_flutter_libs/.metadata new file mode 100644 index 00000000..6a7dbab7 --- /dev/null +++ b/packages/powersync_flutter_libs/.metadata @@ -0,0 +1,42 @@ +# 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: "54e66469a933b60ddf175f858f82eaeb97e48c8d" + channel: "stable" + +project_type: plugin_ffi + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + - platform: android + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + - platform: ios + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + - platform: linux + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + - platform: macos + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + - platform: windows + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + + # 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_flutter_libs/CHANGELOG.md b/packages/powersync_flutter_libs/CHANGELOG.md new file mode 100644 index 00000000..a6c98e78 --- /dev/null +++ b/packages/powersync_flutter_libs/CHANGELOG.md @@ -0,0 +1,7 @@ +## 0.1.0 + +- Upgrade `powersync-sqlite-core` on Android to version 0.1.7 which lowers the minSDK to API 21 + +## 0.0.1 + +- Load the powersync extension binaries on Android, iOS, macOS, Windows and Linux diff --git a/packages/powersync_flutter_libs/LICENSE b/packages/powersync_flutter_libs/LICENSE new file mode 100644 index 00000000..8318dc07 --- /dev/null +++ b/packages/powersync_flutter_libs/LICENSE @@ -0,0 +1,176 @@ + 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_flutter_libs/NOTICE b/packages/powersync_flutter_libs/NOTICE new file mode 100644 index 00000000..04b7c9ff --- /dev/null +++ b/packages/powersync_flutter_libs/NOTICE @@ -0,0 +1 @@ +Copyright 2023 Journey Mobile, Inc. diff --git a/packages/powersync_flutter_libs/README.md b/packages/powersync_flutter_libs/README.md new file mode 100644 index 00000000..19eba4ac --- /dev/null +++ b/packages/powersync_flutter_libs/README.md @@ -0,0 +1,5 @@ +# powersync_flutter_libs + +### 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 new file mode 100644 index 00000000..a5744c1c --- /dev/null +++ b/packages/powersync_flutter_libs/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/powersync_flutter_libs/android/.gitignore b/packages/powersync_flutter_libs/android/.gitignore new file mode 100644 index 00000000..161bdcda --- /dev/null +++ b/packages/powersync_flutter_libs/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/packages/powersync_flutter_libs/android/build.gradle b/packages/powersync_flutter_libs/android/build.gradle new file mode 100644 index 00000000..08576136 --- /dev/null +++ b/packages/powersync_flutter_libs/android/build.gradle @@ -0,0 +1,54 @@ +// The Android Gradle Plugin builds the native code with the Android NDK. + +group 'com.powersync.powersync_flutter_libs' +version '1.0' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + // The Android Gradle Plugin knows how to build native code with the NDK. + classpath 'com.android.tools.build:gradle:7.3.0' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + if (project.android.hasProperty("namespace")) { + namespace 'com.powersync.powersync_flutter_libs' + } + + // Bumping the plugin compileSdk version requires all clients of this plugin + // to bump the version in their app. + compileSdk 32 + + // Use the NDK version + // declared in /android/app/build.gradle file of the Flutter project. + // Replace it with a version number if this plugin requires a specfic NDK version. + // (e.g. ndkVersion "23.1.7779620") + ndkVersion android.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + minSdkVersion 19 + } +} + +dependencies { + implementation 'co.powersync:powersync-sqlite-core:0.1.7' +} diff --git a/packages/powersync_flutter_libs/android/gradle/wrapper/gradle-wrapper.jar b/packages/powersync_flutter_libs/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..ccebba77 Binary files /dev/null and b/packages/powersync_flutter_libs/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/packages/powersync_flutter_libs/android/gradle/wrapper/gradle-wrapper.properties b/packages/powersync_flutter_libs/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..42defcc9 --- /dev/null +++ b/packages/powersync_flutter_libs/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/packages/powersync_flutter_libs/android/gradlew b/packages/powersync_flutter_libs/android/gradlew new file mode 100755 index 00000000..79a61d42 --- /dev/null +++ b/packages/powersync_flutter_libs/android/gradlew @@ -0,0 +1,244 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/packages/powersync_flutter_libs/android/gradlew.bat b/packages/powersync_flutter_libs/android/gradlew.bat new file mode 100644 index 00000000..6689b85b --- /dev/null +++ b/packages/powersync_flutter_libs/android/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/packages/powersync_flutter_libs/android/settings.gradle b/packages/powersync_flutter_libs/android/settings.gradle new file mode 100644 index 00000000..03a8b88d --- /dev/null +++ b/packages/powersync_flutter_libs/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'powersync_flutter_libs' diff --git a/packages/powersync_flutter_libs/android/src/main/AndroidManifest.xml b/packages/powersync_flutter_libs/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..804782c4 --- /dev/null +++ b/packages/powersync_flutter_libs/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/packages/powersync_flutter_libs/android/src/main/java/com/powersync/powersync_flutter_libs/PowersyncFlutterLibsPlugin.java b/packages/powersync_flutter_libs/android/src/main/java/com/powersync/powersync_flutter_libs/PowersyncFlutterLibsPlugin.java new file mode 100644 index 00000000..54d7d425 --- /dev/null +++ b/packages/powersync_flutter_libs/android/src/main/java/com/powersync/powersync_flutter_libs/PowersyncFlutterLibsPlugin.java @@ -0,0 +1,13 @@ +package com.powersync.powersync_flutter_libs; + +import androidx.annotation.NonNull; + +import io.flutter.embedding.engine.plugins.FlutterPlugin; + +public class PowersyncFlutterLibsPlugin implements FlutterPlugin { + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { } + + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { } +} diff --git a/packages/powersync_flutter_libs/example/README.md b/packages/powersync_flutter_libs/example/README.md new file mode 100644 index 00000000..a735a95a --- /dev/null +++ b/packages/powersync_flutter_libs/example/README.md @@ -0,0 +1 @@ +This example is only included for Pana package score checks. This package should automatically apply native binaries if installed. diff --git a/packages/powersync_flutter_libs/ios/Classes/PowersyncFlutterLibsPlugin.h b/packages/powersync_flutter_libs/ios/Classes/PowersyncFlutterLibsPlugin.h new file mode 100644 index 00000000..0ddfb7f7 --- /dev/null +++ b/packages/powersync_flutter_libs/ios/Classes/PowersyncFlutterLibsPlugin.h @@ -0,0 +1,4 @@ +#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 new file mode 100644 index 00000000..74d2e35c --- /dev/null +++ b/packages/powersync_flutter_libs/ios/Classes/PowersyncFlutterLibsPlugin.m @@ -0,0 +1,7 @@ +#import "PowersyncFlutterLibsPlugin.h" + +@implementation PowersyncFlutterLibsPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + +} +@end diff --git a/packages/powersync_flutter_libs/ios/powersync_flutter_libs.podspec b/packages/powersync_flutter_libs/ios/powersync_flutter_libs.podspec new file mode 100644 index 00000000..b7e2abe2 --- /dev/null +++ b/packages/powersync_flutter_libs/ios/powersync_flutter_libs.podspec @@ -0,0 +1,30 @@ +# +# 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 'Flutter' + s.platform = :ios, '11.0' + + s.dependency "powersync-sqlite-core", "~> 0.1.6" + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' +end diff --git a/packages/powersync_flutter_libs/lib/powersync_flutter_libs.dart b/packages/powersync_flutter_libs/lib/powersync_flutter_libs.dart new file mode 100644 index 00000000..4fe98808 --- /dev/null +++ b/packages/powersync_flutter_libs/lib/powersync_flutter_libs.dart @@ -0,0 +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; diff --git a/packages/powersync_flutter_libs/linux/CMakeLists.txt b/packages/powersync_flutter_libs/linux/CMakeLists.txt new file mode 100644 index 00000000..b499b5ee --- /dev/null +++ b/packages/powersync_flutter_libs/linux/CMakeLists.txt @@ -0,0 +1,52 @@ +cmake_minimum_required(VERSION 3.14) + +# Project-level configuration. +set(PROJECT_NAME "powersync_flutter_libs") +project(${PROJECT_NAME} LANGUAGES CXX) + +# This value is used when generating builds using this plugin, so it must +# not be changed. +set(PLUGIN_NAME "powersync_flutter_libs_plugin") + +# Define the plugin library target. Its name must not be changed (see comment +# on PLUGIN_NAME above). +# +# Any new source files that you add to the plugin should be added here. +add_library(${PLUGIN_NAME} SHARED + "powersync_flutter_libs_plugin.cc" +) + +set_target_properties(${PLUGIN_NAME} PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) +target_include_directories(${PLUGIN_NAME} INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter) + +# ---------------------------------------------------------------------- +# Download and add powersync prebuilt library. + +set(POWERSYNC_VERSION 0.1.6) + +set(POWERSYNC_ARCH ${CMAKE_SYSTEM_PROCESSOR}) +if (${POWERSYNC_ARCH} MATCHES "x86_64" OR ${POWERSYNC_ARCH} MATCHES "AMD64") + set(POWERSYNC_ARCH x64) +elseif (${POWERSYNC_ARCH} MATCHES "^arm64" OR ${POWERSYNC_ARCH} MATCHES "^armv8") + set(POWERSYNC_ARCH aarch64) +endif () + +set(POWERSYNC_FILE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/libpowersync.so") + +file(DOWNLOAD + "https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v${POWERSYNC_VERSION}/libpowersync_${POWERSYNC_ARCH}.so" + ${POWERSYNC_FILE_PATH} +) + +# ---------------------------------------------------------------------- + +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set(powersync_flutter_libs_bundled_libraries + "${POWERSYNC_FILE_PATH}" + PARENT_SCOPE +) diff --git a/packages/powersync_flutter_libs/linux/include/powersync_flutter_libs/powersync_flutter_libs_plugin.h b/packages/powersync_flutter_libs/linux/include/powersync_flutter_libs/powersync_flutter_libs_plugin.h new file mode 100644 index 00000000..ea8591af --- /dev/null +++ b/packages/powersync_flutter_libs/linux/include/powersync_flutter_libs/powersync_flutter_libs_plugin.h @@ -0,0 +1,27 @@ +#ifndef FLUTTER_PLUGIN_POWERSYNC_FLUTTER_LIBS_PLUGIN_H_ +#define FLUTTER_PLUGIN_POWERSYNC_FLUTTER_LIBS_PLUGIN_H_ + +#include + +G_BEGIN_DECLS + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility("default"))) +#else +#define FLUTTER_PLUGIN_EXPORT +#endif + +typedef struct _PowersyncFlutterLibsPlugin PoweryncFlutterLibsPlugin; +typedef struct +{ + GObjectClass parent_class; +} PoweryncFlutterLibsPluginClass; + +FLUTTER_PLUGIN_EXPORT GType powersync_flutter_libs_plugin_get_type(); + +FLUTTER_PLUGIN_EXPORT void powersync_flutter_libs_plugin_register_with_registrar( + FlPluginRegistrar *registrar); + +G_END_DECLS + +#endif // FLUTTER_PLUGIN_POWERSYNC_FLUTTER_LIBS_PLUGIN_H_ diff --git a/packages/powersync_flutter_libs/linux/powersync_flutter_libs_plugin.cc b/packages/powersync_flutter_libs/linux/powersync_flutter_libs_plugin.cc new file mode 100644 index 00000000..74bede91 --- /dev/null +++ b/packages/powersync_flutter_libs/linux/powersync_flutter_libs_plugin.cc @@ -0,0 +1,7 @@ +#include "include/powersync_flutter_libs/powersync_flutter_libs_plugin.h" + +#include + +void powersync_flutter_libs_plugin_register_with_registrar(FlPluginRegistrar *registrar) { + +} \ No newline at end of file diff --git a/packages/powersync_flutter_libs/macos/Classes/PowersyncFlutterLibsPlugin.h b/packages/powersync_flutter_libs/macos/Classes/PowersyncFlutterLibsPlugin.h new file mode 100644 index 00000000..9725b1d6 --- /dev/null +++ b/packages/powersync_flutter_libs/macos/Classes/PowersyncFlutterLibsPlugin.h @@ -0,0 +1,4 @@ +#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 new file mode 100644 index 00000000..74d2e35c --- /dev/null +++ b/packages/powersync_flutter_libs/macos/Classes/PowersyncFlutterLibsPlugin.m @@ -0,0 +1,7 @@ +#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 new file mode 100644 index 00000000..463fb759 --- /dev/null +++ b/packages/powersync_flutter_libs/macos/powersync_flutter_libs.podspec @@ -0,0 +1,29 @@ +# +# 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.1.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 new file mode 100644 index 00000000..6bad998f --- /dev/null +++ b/packages/powersync_flutter_libs/pubspec.yaml @@ -0,0 +1,33 @@ +name: powersync_flutter_libs +description: PowerSync core binaries for the PowerSync Flutter SDK. Needs to be included for Flutter apps. +version: 0.1.0 +repository: https://github.com/powersync-ja/powersync.dart +homepage: https://www.powersync.com/ + +environment: + sdk: ^3.2.3 + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 + +flutter: + plugin: + platforms: + android: + package: com.powersync.powersync_flutter_libs + pluginClass: PowersyncFlutterLibsPlugin + ios: + pluginClass: PowersyncFlutterLibsPlugin + linux: + pluginClass: PowersyncFlutterLibsPlugin + macos: + pluginClass: PowersyncFlutterLibsPlugin + windows: + pluginClass: PowersyncFlutterLibsPlugin diff --git a/packages/powersync_flutter_libs/windows/.gitignore b/packages/powersync_flutter_libs/windows/.gitignore new file mode 100644 index 00000000..b3eb2be1 --- /dev/null +++ b/packages/powersync_flutter_libs/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ + +# 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_flutter_libs/windows/CMakeLists.txt b/packages/powersync_flutter_libs/windows/CMakeLists.txt new file mode 100644 index 00000000..e8e1fad6 --- /dev/null +++ b/packages/powersync_flutter_libs/windows/CMakeLists.txt @@ -0,0 +1,53 @@ +cmake_minimum_required(VERSION 3.14) + +# Project-level configuration. +set(PROJECT_NAME "powersync_flutter_libs") +project(${PROJECT_NAME} LANGUAGES CXX) + +# This value is used when generating builds using this plugin, so it must +# not be changed. +set(PLUGIN_NAME "powersync_flutter_libs_plugin") + +# Define the plugin library target. Its name must not be changed (see comment +# on PLUGIN_NAME above). +# +# Any new source files that you add to the plugin should be added here. +add_library(${PLUGIN_NAME} SHARED + "powersync_flutter_libs_plugin.cpp" +) + +apply_standard_settings(${PLUGIN_NAME}) +set_target_properties(${PLUGIN_NAME} PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) +target_include_directories(${PLUGIN_NAME} INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) + +# ---------------------------------------------------------------------- +# Download and add powersync prebuilt library. + +set(POWERSYNC_VERSION 0.1.6) + +set(POWERSYNC_ARCH ${CMAKE_SYSTEM_PROCESSOR}) +if (${POWERSYNC_ARCH} MATCHES "x86_64" OR ${POWERSYNC_ARCH} MATCHES "AMD64") + set(POWERSYNC_ARCH x64) +elseif (${POWERSYNC_ARCH} MATCHES "^arm64" OR ${POWERSYNC_ARCH} MATCHES "^armv8") + set(POWERSYNC_ARCH aarch64) +endif () + +set(POWERSYNC_FILE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/powersync.dll") + +file(DOWNLOAD + "https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v${POWERSYNC_VERSION}/powersync_${POWERSYNC_ARCH}.dll" + ${POWERSYNC_FILE_PATH} +) + +# ---------------------------------------------------------------------- + +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set(powersync_flutter_libs_bundled_libraries + "${POWERSYNC_FILE_PATH}" + PARENT_SCOPE +) diff --git a/packages/powersync_flutter_libs/windows/include/powersync_flutter_libs/powersync_flutter_libs_plugin.h b/packages/powersync_flutter_libs/windows/include/powersync_flutter_libs/powersync_flutter_libs_plugin.h new file mode 100644 index 00000000..5029e52d --- /dev/null +++ b/packages/powersync_flutter_libs/windows/include/powersync_flutter_libs/powersync_flutter_libs_plugin.h @@ -0,0 +1,23 @@ +#ifndef FLUTTER_PLUGIN_POWERSYNC_FLUTTER_LIBS_PLUGIN_H_ +#define FLUTTER_PLUGIN_POWERSYNC_FLUTTER_LIBS_PLUGIN_H_ + +#include + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +FLUTTER_PLUGIN_EXPORT void PowersyncFlutterLibsPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_POWERSYNC_FLUTTER_LIBS_PLUGIN_H_ \ No newline at end of file diff --git a/packages/powersync_flutter_libs/windows/powersync_flutter_libs_plugin.cpp b/packages/powersync_flutter_libs/windows/powersync_flutter_libs_plugin.cpp new file mode 100644 index 00000000..6506de36 --- /dev/null +++ b/packages/powersync_flutter_libs/windows/powersync_flutter_libs_plugin.cpp @@ -0,0 +1,6 @@ +#include "include/powersync_flutter_libs/powersync_flutter_libs_plugin.h" + +void PowersyncFlutterLibsPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + +} \ No newline at end of file diff --git a/scripts/init_powersync_core_binary.dart b/scripts/init_powersync_core_binary.dart new file mode 100644 index 00000000..3bd78ba3 --- /dev/null +++ b/scripts/init_powersync_core_binary.dart @@ -0,0 +1,93 @@ +/// 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 +import 'dart:ffi'; +import 'dart:io'; + +import 'package:melos/melos.dart'; + +final sqliteUrl = + 'https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v0.1.7'; + +void main() async { + final sqliteCoreFilename = getLibraryForPlatform(); + final powersyncPath = "packages/powersync"; + final sqliteCorePath = '$powersyncPath/$sqliteCoreFilename'; + + // Download dynamic library + await downloadFile("$sqliteUrl/$sqliteCoreFilename", sqliteCorePath); + + final originalFile = File(sqliteCorePath); + + try { + final newFileName = getFileNameForPlatform(); + if (await originalFile.exists()) { + try { + // Rename the original file to the new file name + await originalFile.rename("$powersyncPath/$newFileName"); + print( + 'File renamed successfully from $sqliteCoreFilename to $newFileName'); + } catch (e) { + throw IOException('Error renaming file: $e'); + } + } else { + throw IOException('File $sqliteCoreFilename does not exist.'); + } + } on IOException catch (e) { + print(e.message); + } +} + +String getFileNameForPlatform() { + switch (Abi.current()) { + case Abi.macosArm64: + case Abi.macosX64: + return 'libpowersync.dylib'; + case Abi.linuxX64: + case Abi.linuxArm64: + return 'libpowersync.so'; + case Abi.windowsX64: + return 'powersync.dll'; + default: + throw IOException( + 'Unsupported processor architecture "${Abi.current()}". ' + 'Please open an issue on GitHub to request it.', + ); + } +} + +Future downloadFile(String url, String savePath) async { + print('Downloading: $url'); + var httpClient = HttpClient(); + 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 { + print( + 'Failed to download file: ${response.statusCode} ${response.reasonPhrase}'); + } +} + +String getLibraryForPlatform() { + switch (Abi.current()) { + case Abi.macosArm64: + return 'libpowersync_aarch64.dylib'; + case Abi.macosX64: + return 'libpowersync_x64.dylib'; + case Abi.linuxX64: + return 'libpowersync_x64.so'; + case Abi.linuxArm64: + return 'libpowersync_aarch64.so'; + case Abi.windowsX64: + return 'powersync_x64.dll'; + case Abi.windowsArm64: + throw IOException('ARM64 Windows is not supported. ' + 'Please use an x86_64 Windows machine or open a GitHub issue to request it'); + default: + throw IOException( + 'Unsupported processor architecture "${Abi.current()}". ' + 'Please open an issue on GitHub to request it.', + ); + } +} diff --git a/scripts/init_sqlite_wasm.dart b/scripts/init_sqlite_wasm.dart index 34d94a37..40aa79c0 100644 --- a/scripts/init_sqlite_wasm.dart +++ b/scripts/init_sqlite_wasm.dart @@ -2,7 +2,7 @@ import 'dart:io'; final sqliteUrl = - 'https://github.com/simolus3/sqlite3.dart/releases/download/sqlite3-2.4.3/sqlite3.wasm'; + 'https://github.com/powersync-ja/sqlite3.dart/releases/download/v0.1.0/sqlite3.wasm'; void main() async { // Create assets directory if it doesn't exist