From ecd744b8421c43d244dc48bba20b5bcd10b7054d Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Thu, 17 Jul 2025 15:12:47 +0200 Subject: [PATCH 01/18] initial boilerplate of new attachments package --- demos/benchmarks/pubspec.lock | 6 +- demos/django-todolist/pubspec.lock | 6 +- demos/firebase-nodejs-todolist/pubspec.lock | 6 +- demos/supabase-anonymous-auth/pubspec.lock | 6 +- .../supabase-edge-function-auth/pubspec.lock | 6 +- demos/supabase-simple-chat/pubspec.lock | 6 +- demos/supabase-todolist-drift/pubspec.lock | 8 +- .../pubspec.lock | 6 +- demos/supabase-todolist/.metadata | 45 ++++++ demos/supabase-todolist/android/.gitignore | 3 +- .../android/app/build.gradle | 70 ---------- .../android/app/build.gradle.kts | 44 ++++++ .../android/app/src/debug/AndroidManifest.xml | 7 +- .../android/app/src/main/AndroidManifest.xml | 19 ++- .../co/powersync/demotodolist/MainActivity.kt | 6 - .../powersync_flutter_demo/MainActivity.kt | 5 + .../app/src/profile/AndroidManifest.xml | 7 +- demos/supabase-todolist/android/build.gradle | 31 ----- .../android/build.gradle.kts | 21 +++ .../android/gradle.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../supabase-todolist/android/settings.gradle | 11 -- .../android/settings.gradle.kts | 25 ++++ .../xcshareddata/xcschemes/Runner.xcscheme | 2 + .../ios/RunnerTests/RunnerTests.swift | 12 ++ .../linux/runner/CMakeLists.txt | 26 ++++ demos/supabase-todolist/linux/runner/main.cc | 6 + .../linux/runner/my_application.cc | 130 ++++++++++++++++++ .../linux/runner/my_application.h | 18 +++ .../macos/RunnerTests/RunnerTests.swift | 12 ++ demos/supabase-todolist/pubspec.lock | 17 +-- demos/supabase-todolist/test/widget_test.dart | 30 ++++ demos/supabase-trello/pubspec.lock | 6 +- .../powersync_attachments_stream/.gitignore | 31 +++++ .../powersync_attachments_stream/.metadata | 10 ++ .../powersync_attachments_stream/CHANGELOG.md | 3 + packages/powersync_attachments_stream/LICENSE | 1 + .../powersync_attachments_stream/README.md | 39 ++++++ .../analysis_options.yaml | 4 + .../lib/powersync_attachments_stream.dart | 6 + .../lib/src/attachment_context.dart | 7 + .../lib/src/attachment_queue_service.dart | 66 +++++++++ .../lib/src/attachment_service.dart | 13 ++ .../lib/src/attachment_state.dart | 47 +++++++ .../lib/src/local_storage.dart | 24 ++++ .../lib/src/sync_error_handler.dart | 6 + .../lib/src/utils/mutex.dart | 33 +++++ .../lib/src/utils/streams.dart | 1 + .../powersync_attachments_stream/pubspec.yaml | 30 ++++ .../powersync_attachments_stream_test.dart | 14 ++ .../powersync_sqlcipher/example/pubspec.lock | 31 ++--- 51 files changed, 791 insertions(+), 182 deletions(-) create mode 100644 demos/supabase-todolist/.metadata delete mode 100644 demos/supabase-todolist/android/app/build.gradle create mode 100644 demos/supabase-todolist/android/app/build.gradle.kts delete mode 100644 demos/supabase-todolist/android/app/src/main/kotlin/co/powersync/demotodolist/MainActivity.kt create mode 100644 demos/supabase-todolist/android/app/src/main/kotlin/com/powersync/powersync_flutter_demo/MainActivity.kt delete mode 100644 demos/supabase-todolist/android/build.gradle create mode 100644 demos/supabase-todolist/android/build.gradle.kts delete mode 100644 demos/supabase-todolist/android/settings.gradle create mode 100644 demos/supabase-todolist/android/settings.gradle.kts create mode 100644 demos/supabase-todolist/ios/RunnerTests/RunnerTests.swift create mode 100644 demos/supabase-todolist/linux/runner/CMakeLists.txt create mode 100644 demos/supabase-todolist/linux/runner/main.cc create mode 100644 demos/supabase-todolist/linux/runner/my_application.cc create mode 100644 demos/supabase-todolist/linux/runner/my_application.h create mode 100644 demos/supabase-todolist/macos/RunnerTests/RunnerTests.swift create mode 100644 demos/supabase-todolist/test/widget_test.dart create mode 100644 packages/powersync_attachments_stream/.gitignore create mode 100644 packages/powersync_attachments_stream/.metadata create mode 100644 packages/powersync_attachments_stream/CHANGELOG.md create mode 100644 packages/powersync_attachments_stream/LICENSE create mode 100644 packages/powersync_attachments_stream/README.md create mode 100644 packages/powersync_attachments_stream/analysis_options.yaml create mode 100644 packages/powersync_attachments_stream/lib/powersync_attachments_stream.dart create mode 100644 packages/powersync_attachments_stream/lib/src/attachment_context.dart create mode 100644 packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart create mode 100644 packages/powersync_attachments_stream/lib/src/attachment_service.dart create mode 100644 packages/powersync_attachments_stream/lib/src/attachment_state.dart create mode 100644 packages/powersync_attachments_stream/lib/src/local_storage.dart create mode 100644 packages/powersync_attachments_stream/lib/src/sync_error_handler.dart create mode 100644 packages/powersync_attachments_stream/lib/src/utils/mutex.dart create mode 100644 packages/powersync_attachments_stream/lib/src/utils/streams.dart create mode 100644 packages/powersync_attachments_stream/pubspec.yaml create mode 100644 packages/powersync_attachments_stream/test/powersync_attachments_stream_test.dart diff --git a/demos/benchmarks/pubspec.lock b/demos/benchmarks/pubspec.lock index 78420348..609f64fe 100644 --- a/demos/benchmarks/pubspec.lock +++ b/demos/benchmarks/pubspec.lock @@ -281,21 +281,21 @@ packages: path: "../../packages/powersync" relative: true source: path - version: "1.13.0" + version: "1.14.1" powersync_core: dependency: "direct overridden" description: path: "../../packages/powersync_core" relative: true source: path - version: "1.3.0" + version: "1.4.1" powersync_flutter_libs: dependency: "direct overridden" description: path: "../../packages/powersync_flutter_libs" relative: true source: path - version: "0.4.8" + version: "0.4.9" pub_semver: dependency: transitive description: diff --git a/demos/django-todolist/pubspec.lock b/demos/django-todolist/pubspec.lock index e627551d..cf6eb60c 100644 --- a/demos/django-todolist/pubspec.lock +++ b/demos/django-todolist/pubspec.lock @@ -294,21 +294,21 @@ packages: path: "../../packages/powersync" relative: true source: path - version: "1.13.0" + version: "1.14.1" powersync_core: dependency: "direct overridden" description: path: "../../packages/powersync_core" relative: true source: path - version: "1.3.0" + version: "1.4.1" powersync_flutter_libs: dependency: "direct overridden" description: path: "../../packages/powersync_flutter_libs" relative: true source: path - version: "0.4.8" + version: "0.4.9" pub_semver: dependency: transitive description: diff --git a/demos/firebase-nodejs-todolist/pubspec.lock b/demos/firebase-nodejs-todolist/pubspec.lock index 96bd7e21..e9854266 100644 --- a/demos/firebase-nodejs-todolist/pubspec.lock +++ b/demos/firebase-nodejs-todolist/pubspec.lock @@ -430,21 +430,21 @@ packages: path: "../../packages/powersync" relative: true source: path - version: "1.13.0" + version: "1.14.1" powersync_core: dependency: "direct overridden" description: path: "../../packages/powersync_core" relative: true source: path - version: "1.3.0" + version: "1.4.1" powersync_flutter_libs: dependency: "direct overridden" description: path: "../../packages/powersync_flutter_libs" relative: true source: path - version: "0.4.8" + version: "0.4.9" pub_semver: dependency: transitive description: diff --git a/demos/supabase-anonymous-auth/pubspec.lock b/demos/supabase-anonymous-auth/pubspec.lock index e3838774..326cabca 100644 --- a/demos/supabase-anonymous-auth/pubspec.lock +++ b/demos/supabase-anonymous-auth/pubspec.lock @@ -374,21 +374,21 @@ packages: path: "../../packages/powersync" relative: true source: path - version: "1.13.0" + version: "1.14.1" powersync_core: dependency: "direct overridden" description: path: "../../packages/powersync_core" relative: true source: path - version: "1.3.0" + version: "1.4.1" powersync_flutter_libs: dependency: "direct overridden" description: path: "../../packages/powersync_flutter_libs" relative: true source: path - version: "0.4.8" + version: "0.4.9" pub_semver: dependency: transitive description: diff --git a/demos/supabase-edge-function-auth/pubspec.lock b/demos/supabase-edge-function-auth/pubspec.lock index e3838774..326cabca 100644 --- a/demos/supabase-edge-function-auth/pubspec.lock +++ b/demos/supabase-edge-function-auth/pubspec.lock @@ -374,21 +374,21 @@ packages: path: "../../packages/powersync" relative: true source: path - version: "1.13.0" + version: "1.14.1" powersync_core: dependency: "direct overridden" description: path: "../../packages/powersync_core" relative: true source: path - version: "1.3.0" + version: "1.4.1" powersync_flutter_libs: dependency: "direct overridden" description: path: "../../packages/powersync_flutter_libs" relative: true source: path - version: "0.4.8" + version: "0.4.9" pub_semver: dependency: transitive description: diff --git a/demos/supabase-simple-chat/pubspec.lock b/demos/supabase-simple-chat/pubspec.lock index 8ea11c3d..ba9b6e65 100644 --- a/demos/supabase-simple-chat/pubspec.lock +++ b/demos/supabase-simple-chat/pubspec.lock @@ -390,21 +390,21 @@ packages: path: "../../packages/powersync" relative: true source: path - version: "1.13.0" + version: "1.14.1" powersync_core: dependency: "direct overridden" description: path: "../../packages/powersync_core" relative: true source: path - version: "1.3.0" + version: "1.4.1" powersync_flutter_libs: dependency: "direct overridden" description: path: "../../packages/powersync_flutter_libs" relative: true source: path - version: "0.4.8" + version: "0.4.9" pub_semver: dependency: transitive description: diff --git a/demos/supabase-todolist-drift/pubspec.lock b/demos/supabase-todolist-drift/pubspec.lock index 1a2be52e..e9b7eb16 100644 --- a/demos/supabase-todolist-drift/pubspec.lock +++ b/demos/supabase-todolist-drift/pubspec.lock @@ -742,28 +742,28 @@ packages: path: "../../packages/powersync" relative: true source: path - version: "1.13.0" + version: "1.14.1" powersync_attachments_helper: dependency: "direct main" description: path: "../../packages/powersync_attachments_helper" relative: true source: path - version: "0.6.18+7" + version: "0.6.18+10" powersync_core: dependency: "direct overridden" description: path: "../../packages/powersync_core" relative: true source: path - version: "1.3.0" + version: "1.4.1" powersync_flutter_libs: dependency: "direct overridden" description: path: "../../packages/powersync_flutter_libs" relative: true source: path - version: "0.4.8" + version: "0.4.9" pub_semver: dependency: transitive description: diff --git a/demos/supabase-todolist-optional-sync/pubspec.lock b/demos/supabase-todolist-optional-sync/pubspec.lock index c7877841..eb8eb4f5 100644 --- a/demos/supabase-todolist-optional-sync/pubspec.lock +++ b/demos/supabase-todolist-optional-sync/pubspec.lock @@ -462,21 +462,21 @@ packages: path: "../../packages/powersync" relative: true source: path - version: "1.13.0" + version: "1.14.1" powersync_core: dependency: "direct overridden" description: path: "../../packages/powersync_core" relative: true source: path - version: "1.3.0" + version: "1.4.1" powersync_flutter_libs: dependency: "direct overridden" description: path: "../../packages/powersync_flutter_libs" relative: true source: path - version: "0.4.8" + version: "0.4.9" pub_semver: dependency: transitive description: diff --git a/demos/supabase-todolist/.metadata b/demos/supabase-todolist/.metadata new file mode 100644 index 00000000..6a623a4e --- /dev/null +++ b/demos/supabase-todolist/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "d7b523b356d15fb81e7d340bbe52b47f93937323" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: android + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: ios + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: linux + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: macos + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: web + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: windows + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/demos/supabase-todolist/android/.gitignore b/demos/supabase-todolist/android/.gitignore index 6f568019..be3943c9 100644 --- a/demos/supabase-todolist/android/.gitignore +++ b/demos/supabase-todolist/android/.gitignore @@ -5,9 +5,10 @@ gradle-wrapper.jar /gradlew.bat /local.properties GeneratedPluginRegistrant.java +.cxx/ # Remember to never publicly share your keystore. -# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +# See https://flutter.dev/to/reference-keystore key.properties **/*.keystore **/*.jks diff --git a/demos/supabase-todolist/android/app/build.gradle b/demos/supabase-todolist/android/app/build.gradle deleted file mode 100644 index 9daa778b..00000000 --- a/demos/supabase-todolist/android/app/build.gradle +++ /dev/null @@ -1,70 +0,0 @@ -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/supabase-todolist/android/app/build.gradle.kts b/demos/supabase-todolist/android/app/build.gradle.kts new file mode 100644 index 00000000..512cc9e7 --- /dev/null +++ b/demos/supabase-todolist/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.powersync.powersync_flutter_demo" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.powersync.powersync_flutter_demo" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/demos/supabase-todolist/android/app/src/debug/AndroidManifest.xml b/demos/supabase-todolist/android/app/src/debug/AndroidManifest.xml index f19dd7d6..399f6981 100644 --- a/demos/supabase-todolist/android/app/src/debug/AndroidManifest.xml +++ b/demos/supabase-todolist/android/app/src/debug/AndroidManifest.xml @@ -1,4 +1,7 @@ - + + diff --git a/demos/supabase-todolist/android/app/src/main/AndroidManifest.xml b/demos/supabase-todolist/android/app/src/main/AndroidManifest.xml index 55e175c4..e85f03e3 100644 --- a/demos/supabase-todolist/android/app/src/main/AndroidManifest.xml +++ b/demos/supabase-todolist/android/app/src/main/AndroidManifest.xml @@ -1,13 +1,13 @@ - - + + + + + + + + diff --git a/demos/supabase-todolist/android/app/src/main/kotlin/co/powersync/demotodolist/MainActivity.kt b/demos/supabase-todolist/android/app/src/main/kotlin/co/powersync/demotodolist/MainActivity.kt deleted file mode 100644 index 88fda765..00000000 --- a/demos/supabase-todolist/android/app/src/main/kotlin/co/powersync/demotodolist/MainActivity.kt +++ /dev/null @@ -1,6 +0,0 @@ -package co.powersync.demotodolist - -import io.flutter.embedding.android.FlutterActivity - -class MainActivity: FlutterActivity() { -} diff --git a/demos/supabase-todolist/android/app/src/main/kotlin/com/powersync/powersync_flutter_demo/MainActivity.kt b/demos/supabase-todolist/android/app/src/main/kotlin/com/powersync/powersync_flutter_demo/MainActivity.kt new file mode 100644 index 00000000..46cf42a6 --- /dev/null +++ b/demos/supabase-todolist/android/app/src/main/kotlin/com/powersync/powersync_flutter_demo/MainActivity.kt @@ -0,0 +1,5 @@ +package com.powersync.powersync_flutter_demo + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/demos/supabase-todolist/android/app/src/profile/AndroidManifest.xml b/demos/supabase-todolist/android/app/src/profile/AndroidManifest.xml index f19dd7d6..399f6981 100644 --- a/demos/supabase-todolist/android/app/src/profile/AndroidManifest.xml +++ b/demos/supabase-todolist/android/app/src/profile/AndroidManifest.xml @@ -1,4 +1,7 @@ - + + diff --git a/demos/supabase-todolist/android/build.gradle b/demos/supabase-todolist/android/build.gradle deleted file mode 100644 index 713d7f6e..00000000 --- a/demos/supabase-todolist/android/build.gradle +++ /dev/null @@ -1,31 +0,0 @@ -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/supabase-todolist/android/build.gradle.kts b/demos/supabase-todolist/android/build.gradle.kts new file mode 100644 index 00000000..89176ef4 --- /dev/null +++ b/demos/supabase-todolist/android/build.gradle.kts @@ -0,0 +1,21 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/demos/supabase-todolist/android/gradle.properties b/demos/supabase-todolist/android/gradle.properties index 94adc3a3..f018a618 100644 --- a/demos/supabase-todolist/android/gradle.properties +++ b/demos/supabase-todolist/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true diff --git a/demos/supabase-todolist/android/gradle/wrapper/gradle-wrapper.properties b/demos/supabase-todolist/android/gradle/wrapper/gradle-wrapper.properties index 3c472b99..ac3b4792 100644 --- a/demos/supabase-todolist/android/gradle/wrapper/gradle-wrapper.properties +++ b/demos/supabase-todolist/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/demos/supabase-todolist/android/settings.gradle b/demos/supabase-todolist/android/settings.gradle deleted file mode 100644 index 44e62bcf..00000000 --- a/demos/supabase-todolist/android/settings.gradle +++ /dev/null @@ -1,11 +0,0 @@ -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/supabase-todolist/android/settings.gradle.kts b/demos/supabase-todolist/android/settings.gradle.kts new file mode 100644 index 00000000..ab39a10a --- /dev/null +++ b/demos/supabase-todolist/android/settings.gradle.kts @@ -0,0 +1,25 @@ +pluginManagement { + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.7.3" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false +} + +include(":app") diff --git a/demos/supabase-todolist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demos/supabase-todolist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c53e2b31..9c12df59 100644 --- a/demos/supabase-todolist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/demos/supabase-todolist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "powersync_flutter_demo"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "powersync_flutter_demo"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/demos/supabase-todolist/linux/runner/my_application.h b/demos/supabase-todolist/linux/runner/my_application.h new file mode 100644 index 00000000..72271d5e --- /dev/null +++ b/demos/supabase-todolist/linux/runner/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/demos/supabase-todolist/macos/RunnerTests/RunnerTests.swift b/demos/supabase-todolist/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..61f3bd1f --- /dev/null +++ b/demos/supabase-todolist/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/demos/supabase-todolist/pubspec.lock b/demos/supabase-todolist/pubspec.lock index 7522f0ed..9294aef3 100644 --- a/demos/supabase-todolist/pubspec.lock +++ b/demos/supabase-todolist/pubspec.lock @@ -462,28 +462,28 @@ packages: path: "../../packages/powersync" relative: true source: path - version: "1.13.0" + version: "1.14.1" powersync_attachments_helper: dependency: "direct main" description: path: "../../packages/powersync_attachments_helper" relative: true source: path - version: "0.6.18+7" + version: "0.6.18+10" powersync_core: dependency: "direct overridden" description: path: "../../packages/powersync_core" relative: true source: path - version: "1.3.0" + version: "1.4.1" powersync_flutter_libs: dependency: "direct overridden" description: path: "../../packages/powersync_flutter_libs" relative: true source: path - version: "0.4.8" + version: "0.4.9" pub_semver: dependency: transitive description: @@ -628,10 +628,11 @@ packages: sqlite_async: dependency: "direct main" description: - path: "/Users/simon/src/sqlite_async.dart/packages/sqlite_async" - relative: false - source: path - version: "0.11.5" + name: sqlite_async + sha256: "9332aedd311a19dd215dcb55729bc68dc587dc7655b569ab8819b68ee0be0082" + url: "https://pub.dev" + source: hosted + version: "0.11.7" stack_trace: dependency: transitive description: diff --git a/demos/supabase-todolist/test/widget_test.dart b/demos/supabase-todolist/test/widget_test.dart new file mode 100644 index 00000000..77913e2e --- /dev/null +++ b/demos/supabase-todolist/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:powersync_flutter_demo/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp(loggedIn: true,)); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/demos/supabase-trello/pubspec.lock b/demos/supabase-trello/pubspec.lock index d101da0a..e3871593 100644 --- a/demos/supabase-trello/pubspec.lock +++ b/demos/supabase-trello/pubspec.lock @@ -542,21 +542,21 @@ packages: path: "../../packages/powersync" relative: true source: path - version: "1.13.0" + version: "1.14.1" powersync_core: dependency: "direct overridden" description: path: "../../packages/powersync_core" relative: true source: path - version: "1.3.0" + version: "1.4.1" powersync_flutter_libs: dependency: "direct overridden" description: path: "../../packages/powersync_flutter_libs" relative: true source: path - version: "0.4.8" + version: "0.4.9" provider: dependency: "direct main" description: diff --git a/packages/powersync_attachments_stream/.gitignore b/packages/powersync_attachments_stream/.gitignore new file mode 100644 index 00000000..eb6c05cd --- /dev/null +++ b/packages/powersync_attachments_stream/.gitignore @@ -0,0 +1,31 @@ +# 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/ +.flutter-plugins +.flutter-plugins-dependencies +build/ diff --git a/packages/powersync_attachments_stream/.metadata b/packages/powersync_attachments_stream/.metadata new file mode 100644 index 00000000..8c47ce66 --- /dev/null +++ b/packages/powersync_attachments_stream/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "d7b523b356d15fb81e7d340bbe52b47f93937323" + channel: "stable" + +project_type: package diff --git a/packages/powersync_attachments_stream/CHANGELOG.md b/packages/powersync_attachments_stream/CHANGELOG.md new file mode 100644 index 00000000..41cc7d81 --- /dev/null +++ b/packages/powersync_attachments_stream/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/powersync_attachments_stream/LICENSE b/packages/powersync_attachments_stream/LICENSE new file mode 100644 index 00000000..ba75c69f --- /dev/null +++ b/packages/powersync_attachments_stream/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/packages/powersync_attachments_stream/README.md b/packages/powersync_attachments_stream/README.md new file mode 100644 index 00000000..4a260d8d --- /dev/null +++ b/packages/powersync_attachments_stream/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/packages/powersync_attachments_stream/analysis_options.yaml b/packages/powersync_attachments_stream/analysis_options.yaml new file mode 100644 index 00000000..a5744c1c --- /dev/null +++ b/packages/powersync_attachments_stream/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_attachments_stream/lib/powersync_attachments_stream.dart b/packages/powersync_attachments_stream/lib/powersync_attachments_stream.dart new file mode 100644 index 00000000..730bb4f4 --- /dev/null +++ b/packages/powersync_attachments_stream/lib/powersync_attachments_stream.dart @@ -0,0 +1,6 @@ +export 'src/attachment_service.dart'; +export 'src/attachment_context.dart'; +export 'src/attachment_state.dart'; +export 'src/local_storage.dart'; +export 'src/sync_error_handler.dart'; +export 'src/attachment_queue_service.dart'; \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/src/attachment_context.dart b/packages/powersync_attachments_stream/lib/src/attachment_context.dart new file mode 100644 index 00000000..3b31c14b --- /dev/null +++ b/packages/powersync_attachments_stream/lib/src/attachment_context.dart @@ -0,0 +1,7 @@ +class AttachmentContext { + // Used for transactional/exclusive operations + Future runExclusive(Future Function() action) async { + // Implement async mutex/lock here if needed + return await action(); + } +} \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart new file mode 100644 index 00000000..840251a4 --- /dev/null +++ b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart @@ -0,0 +1,66 @@ +import 'attachment_service.dart'; +import 'attachment_state.dart'; +import 'attachment_context.dart'; +import 'local_storage.dart'; +import 'sync_error_handler.dart'; +import 'utils/mutex.dart'; +import 'dart:async'; + +class AttachmentQueueService implements AttachmentService { + final LocalStorage localStorage; + final SyncErrorHandler errorHandler; + final AsyncMutex _mutex = AsyncMutex(); + + final StreamController> _activeAttachmentsController = StreamController.broadcast(); + final List _attachments = []; + + AttachmentQueueService({ + required this.localStorage, + required this.errorHandler, + }); + + @override + Future init() async { + // Initialize resources, DB, etc. + } + + @override + Future close() async { + await _activeAttachmentsController.close(); + // Clean up resources + } + + @override + Stream> watchActiveAttachments() => _activeAttachmentsController.stream; + + @override + Future> getActiveAttachments({AttachmentState? state}) async { + return _attachments.where((a) => + a.state != AttachmentState.archived && + (state == null || a.state == state) + ).toList(); + } + + @override + Future triggerSync() async { + // Implement sync logic, update states, handle errors + } + + @override + Future> getAttachments({int? limit, int? offset}) async { + var list = List.from(_attachments); + if (offset != null) list = list.skip(offset).toList(); + if (limit != null) list = list.take(limit).toList(); + return list; + } + + @override + Future withContext(Future Function(AttachmentContext ctx) action) async { + await _mutex.protect(() async { + final ctx = AttachmentContext(); + await action(ctx); + }); + } + + // Add methods for adding, updating, removing attachments, etc. +} \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/src/attachment_service.dart b/packages/powersync_attachments_stream/lib/src/attachment_service.dart new file mode 100644 index 00000000..9c85af8c --- /dev/null +++ b/packages/powersync_attachments_stream/lib/src/attachment_service.dart @@ -0,0 +1,13 @@ +import 'attachment_state.dart'; +import 'attachment_context.dart'; + +abstract class AttachmentService { + Future init(); + Future close(); + + Stream> watchActiveAttachments(); + Future> getActiveAttachments({AttachmentState? state}); + Future triggerSync(); + Future> getAttachments({int? limit, int? offset}); + Future withContext(Future Function(AttachmentContext ctx) action); +} \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/src/attachment_state.dart b/packages/powersync_attachments_stream/lib/src/attachment_state.dart new file mode 100644 index 00000000..f90cffda --- /dev/null +++ b/packages/powersync_attachments_stream/lib/src/attachment_state.dart @@ -0,0 +1,47 @@ +enum AttachmentState { + pending, + uploading, + uploaded, + failed, + archived, + synced, // New state +} + +class Attachment { + final String id; + final AttachmentState state; + final bool hasSynced; + final Map? metaData; + final String? mediaType; + final DateTime? createdAt; + final DateTime? updatedAt; + + Attachment({ + required this.id, + required this.state, + this.hasSynced = false, + this.metaData, + this.mediaType, + this.createdAt, + this.updatedAt, + }); + + Attachment copyWith({ + AttachmentState? state, + bool? hasSynced, + Map? metaData, + String? mediaType, + DateTime? createdAt, + DateTime? updatedAt, + }) { + return Attachment( + id: id, + state: state ?? this.state, + hasSynced: hasSynced ?? this.hasSynced, + metaData: metaData ?? this.metaData, + mediaType: mediaType ?? this.mediaType, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + } +} \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/src/local_storage.dart b/packages/powersync_attachments_stream/lib/src/local_storage.dart new file mode 100644 index 00000000..f1fdf63e --- /dev/null +++ b/packages/powersync_attachments_stream/lib/src/local_storage.dart @@ -0,0 +1,24 @@ +abstract class LocalStorage { + Future save(String id, List bytes, {String? mediaType, Map? metadata}); + Future?> read(String id); + Future delete(String id); + // ... other methods +} + +// Example: IO implementation (can be in a separate file, not imported by default) +class IOLocalStorage implements LocalStorage { + @override + Future save(String id, List bytes, {String? mediaType, Map? metadata}) { + throw UnimplementedError(); + } + + @override + Future?> read(String id) { + throw UnimplementedError(); + } + + @override + Future delete(String id) { + throw UnimplementedError(); + } +} \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/src/sync_error_handler.dart b/packages/powersync_attachments_stream/lib/src/sync_error_handler.dart new file mode 100644 index 00000000..89ffe00b --- /dev/null +++ b/packages/powersync_attachments_stream/lib/src/sync_error_handler.dart @@ -0,0 +1,6 @@ +import 'attachment_state.dart'; + +abstract class SyncErrorHandler { + /// Return true to retry, false to skip/fail + Future onError(Object error, StackTrace stack, {Attachment? attachment}); +} \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/src/utils/mutex.dart b/packages/powersync_attachments_stream/lib/src/utils/mutex.dart new file mode 100644 index 00000000..d62c919e --- /dev/null +++ b/packages/powersync_attachments_stream/lib/src/utils/mutex.dart @@ -0,0 +1,33 @@ +// lib/src/utils/mutex.dart + +import 'dart:async'; + +class AsyncMutex { + bool _locked = false; + final List _queue = []; + + Future protect(Future Function() action) async { + if (_locked) { + final completer = Completer(); + _queue.add(() async { + try { + completer.complete(await action()); + } catch (e, st) { + completer.completeError(e, st); + } + }); + return completer.future; + } else { + _locked = true; + try { + return await action(); + } finally { + _locked = false; + if (_queue.isNotEmpty) { + final next = _queue.removeAt(0); + next(); + } + } + } + } +} \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/src/utils/streams.dart b/packages/powersync_attachments_stream/lib/src/utils/streams.dart new file mode 100644 index 00000000..126192f1 --- /dev/null +++ b/packages/powersync_attachments_stream/lib/src/utils/streams.dart @@ -0,0 +1 @@ +// Utilities for stream management, e.g., debouncing, distinct, etc. \ No newline at end of file diff --git a/packages/powersync_attachments_stream/pubspec.yaml b/packages/powersync_attachments_stream/pubspec.yaml new file mode 100644 index 00000000..5754916b --- /dev/null +++ b/packages/powersync_attachments_stream/pubspec.yaml @@ -0,0 +1,30 @@ +name: powersync_attachments_stream +description: "A helper library for handling attachments when using PowerSync." +version: 0.0.1 +repository: https://github.com/powersync-ja/powersync.dart +homepage: https://www.powersync.com/ + +environment: + sdk: ^3.8.1 + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + + powersync_core: ^1.4.1 + logging: ^1.2.0 + sqlite_async: ^0.11.0 + path_provider: ^2.0.13 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +platforms: + android: + ios: + linux: + macos: + windows: diff --git a/packages/powersync_attachments_stream/test/powersync_attachments_stream_test.dart b/packages/powersync_attachments_stream/test/powersync_attachments_stream_test.dart new file mode 100644 index 00000000..2cbdafd9 --- /dev/null +++ b/packages/powersync_attachments_stream/test/powersync_attachments_stream_test.dart @@ -0,0 +1,14 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:powersync_attachments_stream/powersync_attachments_stream.dart'; + +void main() { + // group('Calculator tests', () { + // test('adds one to input values', () { + // final calculator = Calculator(); + // expect(calculator.addOne(2), 3); + // expect(calculator.addOne(-7), -6); + // expect(calculator.addOne(0), 1); + // }); + // }); +} diff --git a/packages/powersync_sqlcipher/example/pubspec.lock b/packages/powersync_sqlcipher/example/pubspec.lock index 068a9f39..d961ce3c 100644 --- a/packages/powersync_sqlcipher/example/pubspec.lock +++ b/packages/powersync_sqlcipher/example/pubspec.lock @@ -315,29 +315,26 @@ packages: source: hosted version: "2.1.8" powersync_core: - dependency: transitive + dependency: "direct overridden" description: - name: powersync_core - sha256: ad6ffccb5c1e89a9391124a63cd94698946c9bb440a1205725b574c5766b102d - url: "https://pub.dev" - source: hosted - version: "1.3.1" + path: "../../powersync_core" + relative: true + source: path + version: "1.4.1" powersync_flutter_libs: - dependency: transitive + dependency: "direct overridden" description: - name: powersync_flutter_libs - sha256: f1cd9f3a084a39007dd2fb414b03fcc29ab61f0af5d117263fe5f54e407d97d7 - url: "https://pub.dev" - source: hosted - version: "0.4.8" + path: "../../powersync_flutter_libs" + relative: true + source: path + version: "0.4.9" powersync_sqlcipher: dependency: "direct main" description: - name: powersync_sqlcipher - sha256: "33b3d67feab5d4dbf484e2c49e11d7b16f7bd406e70e974a2bbbe439ebc382c4" - url: "https://pub.dev" - source: hosted - version: "0.1.7" + path: ".." + relative: true + source: path + version: "0.1.9" process: dependency: transitive description: From d0f2dc308f90ade342f0f0f67b541e73bdafb299 Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Tue, 22 Jul 2025 09:56:15 +0200 Subject: [PATCH 02/18] Update dependencies and enhance LocalStorage interface with metadata handling in powersync_attachments_stream package and added unit tests --- demos/supabase-todolist/pubspec.lock | 184 ++++++++++++ demos/supabase-todolist/pubspec.yaml | 1 + .../lib/src/local_storage.dart | 76 ++++- .../powersync_attachments_stream/pubspec.yaml | 2 + .../test/local_storage.dart | 274 ++++++++++++++++++ .../powersync_attachments_stream_test.dart | 14 - 6 files changed, 529 insertions(+), 22 deletions(-) create mode 100644 packages/powersync_attachments_stream/test/local_storage.dart delete mode 100644 packages/powersync_attachments_stream/test/powersync_attachments_stream_test.dart diff --git a/demos/supabase-todolist/pubspec.lock b/demos/supabase-todolist/pubspec.lock index 9294aef3..1971bfd9 100644 --- a/demos/supabase-todolist/pubspec.lock +++ b/demos/supabase-todolist/pubspec.lock @@ -1,6 +1,22 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f + url: "https://pub.dev" + source: hosted + version: "85.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: b1ade5707ab7a90dfd519eaac78a7184341d19adb6096c68d499b59c7c6cf880 + url: "https://pub.dev" + source: hosted + version: "7.7.0" app_links: dependency: transitive description: @@ -121,6 +137,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" clock: dependency: transitive description: @@ -137,6 +161,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + url: "https://pub.dev" + source: hosted + version: "1.15.0" cross_file: dependency: transitive description: @@ -216,6 +256,14 @@ packages: description: flutter source: sdk version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" functions_client: dependency: transitive description: @@ -224,6 +272,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.2" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" gotrue: dependency: transitive description: @@ -248,6 +304,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" http_parser: dependency: transitive description: @@ -264,6 +328,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.4" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" json_annotation: dependency: transitive description: @@ -360,6 +440,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" path: dependency: "direct main" description: @@ -440,6 +536,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" posix: dependency: transitive description: @@ -580,11 +684,59 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" source_span: dependency: transitive description: @@ -697,6 +849,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.2" + test: + dependency: "direct dev" + description: + name: test + sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e" + url: "https://pub.dev" + source: hosted + version: "1.25.15" test_api: dependency: transitive description: @@ -705,6 +865,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.4" + test_core: + dependency: transitive + description: + name: test_core + sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" + url: "https://pub.dev" + source: hosted + version: "0.6.8" typed_data: dependency: transitive description: @@ -809,6 +977,14 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" web: dependency: transitive description: @@ -833,6 +1009,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" xdg_directories: dependency: transitive description: diff --git a/demos/supabase-todolist/pubspec.yaml b/demos/supabase-todolist/pubspec.yaml index 9230244e..2f9409fd 100644 --- a/demos/supabase-todolist/pubspec.yaml +++ b/demos/supabase-todolist/pubspec.yaml @@ -26,6 +26,7 @@ dev_dependencies: sdk: flutter flutter_lints: ^3.0.1 + test: ^1.25.15 flutter: uses-material-design: true diff --git a/packages/powersync_attachments_stream/lib/src/local_storage.dart b/packages/powersync_attachments_stream/lib/src/local_storage.dart index f1fdf63e..918b3ed4 100644 --- a/packages/powersync_attachments_stream/lib/src/local_storage.dart +++ b/packages/powersync_attachments_stream/lib/src/local_storage.dart @@ -1,24 +1,84 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:path/path.dart' as p; + abstract class LocalStorage { + /// Saves binary data to storage with an identifier + /// + /// [id] - Unique identifier for the stored data + /// [bytes] - Binary data to store + /// [mediaType] - Optional MIME type of the data (e.g., 'image/jpeg') + /// [metadata] - Optional key-value pairs for additional data information Future save(String id, List bytes, {String? mediaType, Map? metadata}); + + /// Retrieves binary data by identifier + /// + /// [id] - Unique identifier of the data to retrieve + /// Returns the binary data if found, null otherwise Future?> read(String id); + + /// Removes data and its metadata from storage + /// + /// [id] - Unique identifier of the data to delete Future delete(String id); - // ... other methods + + /// Retrieves metadata associated with stored data + /// + /// [id] - Unique identifier of the data + /// Returns metadata map if found, null otherwise + Future?> getMetadata(String id); } -// Example: IO implementation (can be in a separate file, not imported by default) class IOLocalStorage implements LocalStorage { + final Directory baseDir; + + IOLocalStorage(this.baseDir); + + File _fileFor(String id) => File(p.join(baseDir.path, id)); + File _metaFileFor(String id) => File(p.join(baseDir.path, '$id.meta.json')); + + @override + Future save(String id, List bytes, {String? mediaType, Map? metadata}) async { + await baseDir.create(recursive: true); + await _fileFor(id).writeAsBytes(bytes); + if (mediaType != null || metadata != null) { + final meta = {}; + if (mediaType != null) meta['mediaType'] = mediaType; + if (metadata != null) meta['metadata'] = metadata; + await _metaFileFor(id).writeAsString(jsonEncode(meta)); + } + } + @override - Future save(String id, List bytes, {String? mediaType, Map? metadata}) { - throw UnimplementedError(); + Future?> read(String id) async { + final file = _fileFor(id); + if (await file.exists()) { + return await file.readAsBytes(); + } + return null; } @override - Future?> read(String id) { - throw UnimplementedError(); + Future delete(String id) async { + try { + await _fileFor(id).delete(); + } on FileSystemException { + // File doesn't exist, ignore + } + try { + await _metaFileFor(id).delete(); + } on FileSystemException { + // File doesn't exist, ignore + } } @override - Future delete(String id) { - throw UnimplementedError(); + Future?> getMetadata(String id) async { + final metaFile = _metaFileFor(id); + if (await metaFile.exists()) { + final content = await metaFile.readAsString(); + return jsonDecode(content) as Map; + } + return null; } } \ No newline at end of file diff --git a/packages/powersync_attachments_stream/pubspec.yaml b/packages/powersync_attachments_stream/pubspec.yaml index 5754916b..c5e00e5f 100644 --- a/packages/powersync_attachments_stream/pubspec.yaml +++ b/packages/powersync_attachments_stream/pubspec.yaml @@ -11,6 +11,7 @@ environment: dependencies: flutter: sdk: flutter + path: ^1.8.0 powersync_core: ^1.4.1 logging: ^1.2.0 @@ -21,6 +22,7 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^5.0.0 + test: ^1.25.15 platforms: android: diff --git a/packages/powersync_attachments_stream/test/local_storage.dart b/packages/powersync_attachments_stream/test/local_storage.dart new file mode 100644 index 00000000..20a42748 --- /dev/null +++ b/packages/powersync_attachments_stream/test/local_storage.dart @@ -0,0 +1,274 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:test/test.dart'; +import 'package:path/path.dart' as p; +import 'package:powersync_attachments_stream/src/local_storage.dart'; + +void main() { + group('IOLocalStorage', () { + late Directory tempDir; + late IOLocalStorage storage; + + setUp(() async { + tempDir = await Directory.systemTemp.createTemp('local_storage_test_'); + storage = IOLocalStorage(tempDir); + }); + + tearDown(() async { + if (await tempDir.exists()) { + await tempDir.delete(recursive: true); + } + }); + + group('save and read', () { + test('saves and reads binary data successfully', () async { + const id = 'test_file'; + final data = [1, 2, 3, 4, 5]; + + await storage.save(id, data); + final result = await storage.read(id); + + expect(result, equals(data)); + }); + + test('returns null when reading non-existent file', () async { + final result = await storage.read('non_existent'); + expect(result, isNull); + }); + + test('creates base directory if it does not exist', () async { + final nonExistentDir = Directory(p.join(tempDir.path, 'subdir', 'nested')); + final nestedStorage = IOLocalStorage(nonExistentDir); + + expect(await nonExistentDir.exists(), isFalse); + + await nestedStorage.save('test', [1, 2, 3]); + + expect(await nonExistentDir.exists(), isTrue); + final result = await nestedStorage.read('test'); + expect(result, equals([1, 2, 3])); + }); + + test('overwrites existing file', () async { + const id = 'overwrite_test'; + final originalData = [1, 2, 3]; + final newData = [4, 5, 6, 7]; + + await storage.save(id, originalData); + await storage.save(id, newData); + + final result = await storage.read(id); + expect(result, equals(newData)); + }); + }); + + group('metadata', () { + test('saves and retrieves metadata with mediaType only', () async { + const id = 'test_with_media_type'; + final data = [1, 2, 3]; + const mediaType = 'image/jpeg'; + + await storage.save(id, data, mediaType: mediaType); + final metadata = await storage.getMetadata(id); + + expect(metadata, isNotNull); + expect(metadata!['mediaType'], equals(mediaType)); + expect(metadata['metadata'], isNull); + }); + + test('saves and retrieves metadata with custom metadata only', () async { + const id = 'test_with_metadata'; + final data = [1, 2, 3]; + final customMetadata = {'width': 800, 'height': 600, 'format': 'png'}; + + await storage.save(id, data, metadata: customMetadata); + final metadata = await storage.getMetadata(id); + + expect(metadata, isNotNull); + expect(metadata!['mediaType'], isNull); + expect(metadata['metadata'], equals(customMetadata)); + }); + + test('saves and retrieves both mediaType and custom metadata', () async { + const id = 'test_with_both'; + final data = [1, 2, 3]; + const mediaType = 'application/pdf'; + final customMetadata = {'pages': 10, 'author': 'Test Author'}; + + await storage.save(id, data, mediaType: mediaType, metadata: customMetadata); + final metadata = await storage.getMetadata(id); + + expect(metadata, isNotNull); + expect(metadata!['mediaType'], equals(mediaType)); + expect(metadata['metadata'], equals(customMetadata)); + }); + + test('returns null when getting metadata for non-existent file', () async { + final metadata = await storage.getMetadata('non_existent'); + expect(metadata, isNull); + }); + + test('does not create metadata file when no metadata provided', () async { + const id = 'no_metadata'; + final data = [1, 2, 3]; + + await storage.save(id, data); + + final metaFile = File(p.join(tempDir.path, '$id.meta.json')); + expect(await metaFile.exists(), isFalse); + + final metadata = await storage.getMetadata(id); + expect(metadata, isNull); + }); + + test('overwrites existing metadata', () async { + const id = 'metadata_overwrite'; + final data = [1, 2, 3]; + final originalMetadata = {'version': 1}; + final newMetadata = {'version': 2, 'updated': true}; + + await storage.save(id, data, metadata: originalMetadata); + await storage.save(id, data, metadata: newMetadata); + + final metadata = await storage.getMetadata(id); + expect(metadata!['metadata'], equals(newMetadata)); + }); + }); + + group('delete', () { + test('deletes existing file and metadata', () async { + const id = 'delete_test'; + final data = [1, 2, 3]; + final customMetadata = {'test': true}; + + await storage.save(id, data, mediaType: 'text/plain', metadata: customMetadata); + + // Verify files exist + expect(await storage.read(id), isNotNull); + expect(await storage.getMetadata(id), isNotNull); + + await storage.delete(id); + + // Verify files are deleted + expect(await storage.read(id), isNull); + expect(await storage.getMetadata(id), isNull); + }); + + test('does not throw when deleting non-existent files', () async { + // Should not throw exception + await storage.delete('non_existent'); + }); + + test('deletes only data file when metadata does not exist', () async { + const id = 'data_only'; + final data = [1, 2, 3]; + + await storage.save(id, data); // No metadata + expect(await storage.read(id), isNotNull); + + await storage.delete(id); + expect(await storage.read(id), isNull); + }); + + test('handles partial deletion gracefully', () async { + const id = 'partial_delete'; + final data = [1, 2, 3]; + + await storage.save(id, data, metadata: {'test': true}); + + // Manually delete just the data file + final dataFile = File(p.join(tempDir.path, id)); + await dataFile.delete(); + + // Delete should still work without throwing + await storage.delete(id); + + expect(await storage.read(id), isNull); + expect(await storage.getMetadata(id), isNull); + }); + }); + + group('file system integration', () { + test('handles special characters in id', () async { + const id = 'file with spaces & symbols!@#'; + final data = [1, 2, 3]; + + await storage.save(id, data); + final result = await storage.read(id); + + expect(result, equals(data)); + }); + + test('handles empty data', () async { + const id = 'empty_file'; + final data = []; + + await storage.save(id, data); + final result = await storage.read(id); + + expect(result, equals(data)); + }); + + test('handles large binary data', () async { + const id = 'large_file'; + final data = List.generate(10000, (i) => i % 256); + + await storage.save(id, data); + final result = await storage.read(id); + + expect(result, equals(data)); + }); + + test('metadata file has correct JSON format', () async { + const id = 'json_format_test'; + final data = [1, 2, 3]; + const mediaType = 'application/json'; + final customMetadata = {'test': true, 'number': 42}; + + await storage.save(id, data, mediaType: mediaType, metadata: customMetadata); + + // Read metadata file directly + final metaFile = File(p.join(tempDir.path, '$id.meta.json')); + final content = await metaFile.readAsString(); + final parsed = jsonDecode(content) as Map; + + expect(parsed['mediaType'], equals(mediaType)); + expect(parsed['metadata'], equals(customMetadata)); + }); + }); + + group('concurrent operations', () { + test('handles concurrent saves to different files', () async { + final futures = >[]; + + for (int i = 0; i < 10; i++) { + futures.add(storage.save('file_$i', [i, i + 1, i + 2])); + } + + await Future.wait(futures); + + for (int i = 0; i < 10; i++) { + final result = await storage.read('file_$i'); + expect(result, equals([i, i + 1, i + 2])); + } + }); + + test('handles concurrent operations on same file', () async { + const id = 'concurrent_test'; + final data1 = [1, 2, 3]; + final data2 = [4, 5, 6]; + + final futures = [ + storage.save(id, data1), + storage.save(id, data2), + ]; + + await Future.wait(futures); + + final result = await storage.read(id); + expect(result, isNotNull); + expect(result, anyOf(equals(data1), equals(data2))); + }); + }); + }); +} \ No newline at end of file diff --git a/packages/powersync_attachments_stream/test/powersync_attachments_stream_test.dart b/packages/powersync_attachments_stream/test/powersync_attachments_stream_test.dart deleted file mode 100644 index 2cbdafd9..00000000 --- a/packages/powersync_attachments_stream/test/powersync_attachments_stream_test.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; - -import 'package:powersync_attachments_stream/powersync_attachments_stream.dart'; - -void main() { - // group('Calculator tests', () { - // test('adds one to input values', () { - // final calculator = Calculator(); - // expect(calculator.addOne(2), 3); - // expect(calculator.addOne(-7), -6); - // expect(calculator.addOne(0), 1); - // }); - // }); -} From 81dff0813e7a11e3724f87bfce80cf3f9649035c Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Tue, 29 Jul 2025 14:29:17 +0200 Subject: [PATCH 03/18] init attachments package stream and demo with implementation --- .../.gitignore | 52 + .../.metadata | 45 + .../supabase-todolist-new-attachment/LICENSE | 121 ++ .../README.md | 64 + .../analysis_options.yaml | 29 + .../android/.gitignore | 14 + .../android/app/build.gradle.kts | 44 + .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 45 + .../powersync_flutter_demo/MainActivity.kt | 5 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 7 + .../android/build.gradle.kts | 21 + .../android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 5 + .../android/settings.gradle.kts | 25 + .../database.sql | 71 ++ .../flutter_01.png | Bin 0 -> 73026 bytes .../ios/.gitignore | 34 + .../ios/Flutter/AppFrameworkInfo.plist | 26 + .../ios/Flutter/Debug.xcconfig | 2 + .../ios/Flutter/Release.xcconfig | 2 + .../ios/Podfile | 48 + .../ios/Podfile.lock | 89 ++ .../ios/Runner.xcodeproj/project.pbxproj | 552 +++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 90 ++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../ios/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 122 ++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 1418 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + .../ios/Runner/Base.lproj/Main.storyboard | 26 + .../ios/Runner/Info.plist | 55 + .../ios/Runner/Runner-Bridging-Header.h | 1 + .../ios/RunnerTests/RunnerTests.swift | 12 + .../lib/app_config_template.dart | 9 + .../lib/attachments/camera_helpers.dart | 20 + .../lib/attachments/photo_capture_widget.dart | 96 ++ .../lib/attachments/photo_widget.dart | 152 +++ .../lib/attachments/queue.dart | 74 ++ .../attachments/remote_storage_adapter.dart | 74 ++ .../lib/fts_helpers.dart | 17 + .../lib/main.dart | 132 ++ .../lib/migrations/fts_setup.dart | 76 ++ .../lib/migrations/helpers.dart | 38 + .../lib/models/schema.dart | 27 + .../lib/models/todo_item.dart | 51 + .../lib/models/todo_list.dart | 108 ++ .../lib/powersync.dart | 200 ++++ .../lib/supabase.dart | 10 + .../lib/widgets/fts_search_delegate.dart | 111 ++ .../lib/widgets/guard_by_sync.dart | 53 + .../lib/widgets/list_item.dart | 59 + .../lib/widgets/list_item_dialog.dart | 60 + .../lib/widgets/lists_page.dart | 70 ++ .../lib/widgets/login_page.dart | 126 ++ .../lib/widgets/query_widget.dart | 99 ++ .../lib/widgets/resultset_table.dart | 39 + .../lib/widgets/signup_page.dart | 120 ++ .../lib/widgets/status_app_bar.dart | 74 ++ .../lib/widgets/todo_item_dialog.dart | 65 + .../lib/widgets/todo_item_widget.dart | 69 ++ .../lib/widgets/todo_list_page.dart | 68 ++ .../linux/.gitignore | 1 + .../linux/CMakeLists.txt | 138 +++ .../linux/flutter/CMakeLists.txt | 88 ++ .../flutter/generated_plugin_registrant.cc | 27 + .../flutter/generated_plugin_registrant.h | 15 + .../linux/flutter/generated_plugins.cmake | 27 + .../linux/main.cc | 6 + .../linux/my_application.cc | 104 ++ .../linux/my_application.h | 18 + .../linux/runner/CMakeLists.txt | 26 + .../linux/runner/main.cc | 6 + .../linux/runner/my_application.cc | 130 ++ .../linux/runner/my_application.h | 18 + .../macos/.gitignore | 7 + .../macos/Flutter/Flutter-Debug.xcconfig | 2 + .../macos/Flutter/Flutter-Release.xcconfig | 2 + .../Flutter/GeneratedPluginRegistrant.swift | 22 + .../macos/Podfile | 43 + .../macos/Podfile.lock | 83 ++ .../macos/Runner.xcodeproj/project.pbxproj | 834 +++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 99 ++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../macos/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 68 ++ .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 102994 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 5680 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 520 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 14142 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1066 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 36406 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 2218 bytes .../macos/Runner/Base.lproj/MainMenu.xib | 343 ++++++ .../macos/Runner/Configs/AppInfo.xcconfig | 14 + .../macos/Runner/Configs/Debug.xcconfig | 2 + .../macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 14 + .../macos/Runner/Info.plist | 32 + .../macos/Runner/MainFlutterWindow.swift | 15 + .../macos/Runner/Release.entitlements | 10 + .../macos/RunnerTests/RunnerTests.swift | 12 + .../pubspec.lock | 1061 +++++++++++++++++ .../pubspec.yaml | 34 + .../web/favicon.png | Bin 0 -> 917 bytes .../web/icons/Icon-192.png | Bin 0 -> 5292 bytes .../web/icons/Icon-512.png | Bin 0 -> 8252 bytes .../web/icons/Icon-maskable-192.png | Bin 0 -> 5594 bytes .../web/icons/Icon-maskable-512.png | Bin 0 -> 20998 bytes .../web/index.html | 59 + .../web/manifest.json | 35 + .../windows/.gitignore | 17 + .../windows/CMakeLists.txt | 101 ++ .../windows/flutter/CMakeLists.txt | 104 ++ .../flutter/generated_plugin_registrant.cc | 23 + .../flutter/generated_plugin_registrant.h | 15 + .../windows/flutter/generated_plugins.cmake | 27 + .../windows/runner/CMakeLists.txt | 40 + .../windows/runner/Runner.rc | 121 ++ .../windows/runner/flutter_window.cpp | 66 + .../windows/runner/flutter_window.h | 33 + .../windows/runner/main.cpp | 43 + .../windows/runner/resource.h | 16 + .../windows/runner/resources/app_icon.ico | Bin 0 -> 33772 bytes .../windows/runner/runner.exe.manifest | 20 + .../windows/runner/utils.cpp | 64 + .../windows/runner/utils.h | 19 + .../windows/runner/win32_window.cpp | 288 +++++ .../windows/runner/win32_window.h | 102 ++ .../lib/common.dart | 7 + .../lib/powersync_attachments_stream.dart | 13 +- .../src/abstractions/attachment_context.dart | 15 + .../src/abstractions/attachment_service.dart | 6 + .../lib/src/abstractions/local_storage.dart | 44 + .../lib/src/abstractions/remote_storage.dart | 25 + .../lib/src/attachment.dart | 162 +++ .../lib/src/attachment_context.dart | 7 - .../lib/src/attachment_queue_service.dart | 392 +++++- .../lib/src/attachment_service.dart | 13 - .../lib/src/attachment_state.dart | 47 - .../implementations/attachment_context.dart | 191 +++ .../implementations/attachment_service.dart | 63 + .../lib/src/local_storage.dart | 84 -- .../lib/src/storage/io_local_storage.dart | 98 ++ .../lib/src/sync/syncing_service.dart | 283 +++++ .../lib/src/sync_error_handler.dart | 36 +- .../lib/src/utils/mutex.dart | 33 - .../lib/src/utils/streams.dart | 1 - .../powersync_attachments_stream/pubspec.yaml | 9 +- .../test/local_storage.dart | 274 ----- .../test/local_storage_test.dart | 262 ++++ 187 files changed, 9994 insertions(+), 520 deletions(-) create mode 100644 demos/supabase-todolist-new-attachment/.gitignore create mode 100644 demos/supabase-todolist-new-attachment/.metadata create mode 100644 demos/supabase-todolist-new-attachment/LICENSE create mode 100644 demos/supabase-todolist-new-attachment/README.md create mode 100644 demos/supabase-todolist-new-attachment/analysis_options.yaml create mode 100644 demos/supabase-todolist-new-attachment/android/.gitignore create mode 100644 demos/supabase-todolist-new-attachment/android/app/build.gradle.kts create mode 100644 demos/supabase-todolist-new-attachment/android/app/src/debug/AndroidManifest.xml create mode 100644 demos/supabase-todolist-new-attachment/android/app/src/main/AndroidManifest.xml create mode 100644 demos/supabase-todolist-new-attachment/android/app/src/main/kotlin/com/powersync/powersync_flutter_demo/MainActivity.kt create mode 100644 demos/supabase-todolist-new-attachment/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 demos/supabase-todolist-new-attachment/android/app/src/main/res/drawable/launch_background.xml create mode 100644 demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 demos/supabase-todolist-new-attachment/android/app/src/main/res/values-night/styles.xml create mode 100644 demos/supabase-todolist-new-attachment/android/app/src/main/res/values/styles.xml create mode 100644 demos/supabase-todolist-new-attachment/android/app/src/profile/AndroidManifest.xml create mode 100644 demos/supabase-todolist-new-attachment/android/build.gradle.kts create mode 100644 demos/supabase-todolist-new-attachment/android/gradle.properties create mode 100644 demos/supabase-todolist-new-attachment/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 demos/supabase-todolist-new-attachment/android/settings.gradle.kts create mode 100644 demos/supabase-todolist-new-attachment/database.sql create mode 100644 demos/supabase-todolist-new-attachment/flutter_01.png create mode 100644 demos/supabase-todolist-new-attachment/ios/.gitignore create mode 100644 demos/supabase-todolist-new-attachment/ios/Flutter/AppFrameworkInfo.plist create mode 100644 demos/supabase-todolist-new-attachment/ios/Flutter/Debug.xcconfig create mode 100644 demos/supabase-todolist-new-attachment/ios/Flutter/Release.xcconfig create mode 100644 demos/supabase-todolist-new-attachment/ios/Podfile create mode 100644 demos/supabase-todolist-new-attachment/ios/Podfile.lock create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.pbxproj create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/AppDelegate.swift create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Base.lproj/Main.storyboard create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Info.plist create mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Runner-Bridging-Header.h create mode 100644 demos/supabase-todolist-new-attachment/ios/RunnerTests/RunnerTests.swift create mode 100644 demos/supabase-todolist-new-attachment/lib/app_config_template.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/attachments/camera_helpers.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/attachments/photo_capture_widget.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/attachments/photo_widget.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/attachments/queue.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/attachments/remote_storage_adapter.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/fts_helpers.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/main.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/migrations/fts_setup.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/migrations/helpers.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/models/schema.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/models/todo_item.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/models/todo_list.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/powersync.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/supabase.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/fts_search_delegate.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/guard_by_sync.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/list_item.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/list_item_dialog.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/lists_page.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/login_page.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/query_widget.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/resultset_table.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/signup_page.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/status_app_bar.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/todo_item_dialog.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/todo_item_widget.dart create mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/todo_list_page.dart create mode 100644 demos/supabase-todolist-new-attachment/linux/.gitignore create mode 100644 demos/supabase-todolist-new-attachment/linux/CMakeLists.txt create mode 100644 demos/supabase-todolist-new-attachment/linux/flutter/CMakeLists.txt create mode 100644 demos/supabase-todolist-new-attachment/linux/flutter/generated_plugin_registrant.cc create mode 100644 demos/supabase-todolist-new-attachment/linux/flutter/generated_plugin_registrant.h create mode 100644 demos/supabase-todolist-new-attachment/linux/flutter/generated_plugins.cmake create mode 100644 demos/supabase-todolist-new-attachment/linux/main.cc create mode 100644 demos/supabase-todolist-new-attachment/linux/my_application.cc create mode 100644 demos/supabase-todolist-new-attachment/linux/my_application.h create mode 100644 demos/supabase-todolist-new-attachment/linux/runner/CMakeLists.txt create mode 100644 demos/supabase-todolist-new-attachment/linux/runner/main.cc create mode 100644 demos/supabase-todolist-new-attachment/linux/runner/my_application.cc create mode 100644 demos/supabase-todolist-new-attachment/linux/runner/my_application.h create mode 100644 demos/supabase-todolist-new-attachment/macos/.gitignore create mode 100644 demos/supabase-todolist-new-attachment/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 demos/supabase-todolist-new-attachment/macos/Flutter/Flutter-Release.xcconfig create mode 100644 demos/supabase-todolist-new-attachment/macos/Flutter/GeneratedPluginRegistrant.swift create mode 100644 demos/supabase-todolist-new-attachment/macos/Podfile create mode 100644 demos/supabase-todolist-new-attachment/macos/Podfile.lock create mode 100644 demos/supabase-todolist-new-attachment/macos/Runner.xcodeproj/project.pbxproj create mode 100644 demos/supabase-todolist-new-attachment/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 demos/supabase-todolist-new-attachment/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 demos/supabase-todolist-new-attachment/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 demos/supabase-todolist-new-attachment/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/AppDelegate.swift create mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Configs/Debug.xcconfig create mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Configs/Release.xcconfig create mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Configs/Warnings.xcconfig create mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/DebugProfile.entitlements create mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Info.plist create mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/MainFlutterWindow.swift create mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Release.entitlements create mode 100644 demos/supabase-todolist-new-attachment/macos/RunnerTests/RunnerTests.swift create mode 100644 demos/supabase-todolist-new-attachment/pubspec.lock create mode 100644 demos/supabase-todolist-new-attachment/pubspec.yaml create mode 100644 demos/supabase-todolist-new-attachment/web/favicon.png create mode 100644 demos/supabase-todolist-new-attachment/web/icons/Icon-192.png create mode 100644 demos/supabase-todolist-new-attachment/web/icons/Icon-512.png create mode 100644 demos/supabase-todolist-new-attachment/web/icons/Icon-maskable-192.png create mode 100644 demos/supabase-todolist-new-attachment/web/icons/Icon-maskable-512.png create mode 100644 demos/supabase-todolist-new-attachment/web/index.html create mode 100644 demos/supabase-todolist-new-attachment/web/manifest.json create mode 100644 demos/supabase-todolist-new-attachment/windows/.gitignore create mode 100644 demos/supabase-todolist-new-attachment/windows/CMakeLists.txt create mode 100644 demos/supabase-todolist-new-attachment/windows/flutter/CMakeLists.txt create mode 100644 demos/supabase-todolist-new-attachment/windows/flutter/generated_plugin_registrant.cc create mode 100644 demos/supabase-todolist-new-attachment/windows/flutter/generated_plugin_registrant.h create mode 100644 demos/supabase-todolist-new-attachment/windows/flutter/generated_plugins.cmake create mode 100644 demos/supabase-todolist-new-attachment/windows/runner/CMakeLists.txt create mode 100644 demos/supabase-todolist-new-attachment/windows/runner/Runner.rc create mode 100644 demos/supabase-todolist-new-attachment/windows/runner/flutter_window.cpp create mode 100644 demos/supabase-todolist-new-attachment/windows/runner/flutter_window.h create mode 100644 demos/supabase-todolist-new-attachment/windows/runner/main.cpp create mode 100644 demos/supabase-todolist-new-attachment/windows/runner/resource.h create mode 100644 demos/supabase-todolist-new-attachment/windows/runner/resources/app_icon.ico create mode 100644 demos/supabase-todolist-new-attachment/windows/runner/runner.exe.manifest create mode 100644 demos/supabase-todolist-new-attachment/windows/runner/utils.cpp create mode 100644 demos/supabase-todolist-new-attachment/windows/runner/utils.h create mode 100644 demos/supabase-todolist-new-attachment/windows/runner/win32_window.cpp create mode 100644 demos/supabase-todolist-new-attachment/windows/runner/win32_window.h create mode 100644 packages/powersync_attachments_stream/lib/common.dart create mode 100644 packages/powersync_attachments_stream/lib/src/abstractions/attachment_context.dart create mode 100644 packages/powersync_attachments_stream/lib/src/abstractions/attachment_service.dart create mode 100644 packages/powersync_attachments_stream/lib/src/abstractions/local_storage.dart create mode 100644 packages/powersync_attachments_stream/lib/src/abstractions/remote_storage.dart create mode 100644 packages/powersync_attachments_stream/lib/src/attachment.dart delete mode 100644 packages/powersync_attachments_stream/lib/src/attachment_context.dart delete mode 100644 packages/powersync_attachments_stream/lib/src/attachment_service.dart delete mode 100644 packages/powersync_attachments_stream/lib/src/attachment_state.dart create mode 100644 packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart create mode 100644 packages/powersync_attachments_stream/lib/src/implementations/attachment_service.dart delete mode 100644 packages/powersync_attachments_stream/lib/src/local_storage.dart create mode 100644 packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart create mode 100644 packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart delete mode 100644 packages/powersync_attachments_stream/lib/src/utils/mutex.dart delete mode 100644 packages/powersync_attachments_stream/lib/src/utils/streams.dart delete mode 100644 packages/powersync_attachments_stream/test/local_storage.dart create mode 100644 packages/powersync_attachments_stream/test/local_storage_test.dart diff --git a/demos/supabase-todolist-new-attachment/.gitignore b/demos/supabase-todolist-new-attachment/.gitignore new file mode 100644 index 00000000..0f3655d3 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/.gitignore @@ -0,0 +1,52 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.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 \ No newline at end of file diff --git a/demos/supabase-todolist-new-attachment/.metadata b/demos/supabase-todolist-new-attachment/.metadata new file mode 100644 index 00000000..6a623a4e --- /dev/null +++ b/demos/supabase-todolist-new-attachment/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "d7b523b356d15fb81e7d340bbe52b47f93937323" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: android + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: ios + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: linux + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: macos + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: web + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: windows + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/demos/supabase-todolist-new-attachment/LICENSE b/demos/supabase-todolist-new-attachment/LICENSE new file mode 100644 index 00000000..0e259d42 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/LICENSE @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/demos/supabase-todolist-new-attachment/README.md b/demos/supabase-todolist-new-attachment/README.md new file mode 100644 index 00000000..55241dde --- /dev/null +++ b/demos/supabase-todolist-new-attachment/README.md @@ -0,0 +1,64 @@ +# PowerSync + Supabase Flutter Demo: Todo List App + +Demo app demonstrating use of the PowerSync SDK for Flutter together with Supabase. For a step-by-step guide, see [here](https://docs.powersync.com/integration-guides/supabase). + +# Running the app + +Ensure you have [melos](https://melos.invertase.dev/~melos-latest/getting-started) installed. + +1. `cd demos/supabase-todolist` +2. `melos prepare` +3. `cp lib/app_config_template.dart lib/app_config.dart` +4. Insert your Supabase and PowerSync project credentials into `lib/app_config.dart` (See instructions below) +5. `flutter run` + +# Set up Supabase Project + +Create a new Supabase project, and paste an run the contents of [database.sql](./database.sql) in the Supabase SQL editor. + +It does the following: + +1. Create `lists` and `todos` tables. +2. Create a publication called `powersync` for `lists` and `todos`. +3. Enable row level security, allowing users to only view and edit their own data. +4. Create a trigger to populate some sample data when an user registers. + +# Set up PowerSync Instance + +Create a new PowerSync instance, connecting to the database of the Supabase project. + +Then deploy the following sync rules: + +```yaml +bucket_definitions: + user_lists: + priority: 1 + parameters: select id as list_id from lists where owner_id = request.user_id() + data: + - select * from lists where id = bucket.list_id + + user_todos: + parameters: select id as list_id from lists where owner_id = request.user_id() + data: + - select * from todos where list_id = bucket.list_id +``` + +**Note**: These rules showcase [prioritized sync](https://docs.powersync.com/usage/use-case-examples/prioritized-sync), +by syncing a user's lists with a higher priority than the items within a list (todos). This can be +useful to keep the list overview page reactive during a large sync cycle affecting many +rows in the `user_todos` bucket. The two buckets can also be unified into a single one if +priorities are not important (the app will work without changes): + +```yaml +bucket_definitions: + user_lists: + # Separate bucket per todo list + parameters: select id as list_id from lists where owner_id = request.user_id() + data: + - select * from lists where id = bucket.list_id + - select * from todos where list_id = bucket.list_id +``` + +# Configure the app + +Insert the credentials of your new Supabase and PowerSync projects into `lib/app_config.dart` diff --git a/demos/supabase-todolist-new-attachment/analysis_options.yaml b/demos/supabase-todolist-new-attachment/analysis_options.yaml new file mode 100644 index 00000000..61b6c4de --- /dev/null +++ b/demos/supabase-todolist-new-attachment/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/demos/supabase-todolist-new-attachment/android/.gitignore b/demos/supabase-todolist-new-attachment/android/.gitignore new file mode 100644 index 00000000..be3943c9 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/demos/supabase-todolist-new-attachment/android/app/build.gradle.kts b/demos/supabase-todolist-new-attachment/android/app/build.gradle.kts new file mode 100644 index 00000000..512cc9e7 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.powersync.powersync_flutter_demo" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.powersync.powersync_flutter_demo" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/demos/supabase-todolist-new-attachment/android/app/src/debug/AndroidManifest.xml b/demos/supabase-todolist-new-attachment/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/demos/supabase-todolist-new-attachment/android/app/src/main/AndroidManifest.xml b/demos/supabase-todolist-new-attachment/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..e85f03e3 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/supabase-todolist-new-attachment/android/app/src/main/kotlin/com/powersync/powersync_flutter_demo/MainActivity.kt b/demos/supabase-todolist-new-attachment/android/app/src/main/kotlin/com/powersync/powersync_flutter_demo/MainActivity.kt new file mode 100644 index 00000000..46cf42a6 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/android/app/src/main/kotlin/com/powersync/powersync_flutter_demo/MainActivity.kt @@ -0,0 +1,5 @@ +package com.powersync.powersync_flutter_demo + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/demos/supabase-todolist-new-attachment/android/app/src/main/res/drawable-v21/launch_background.xml b/demos/supabase-todolist-new-attachment/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/demos/supabase-todolist-new-attachment/android/app/src/main/res/drawable/launch_background.xml b/demos/supabase-todolist-new-attachment/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/android/app/src/main/res/values-night/styles.xml b/demos/supabase-todolist-new-attachment/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..06952be7 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/demos/supabase-todolist-new-attachment/android/app/src/main/res/values/styles.xml b/demos/supabase-todolist-new-attachment/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..cb1ef880 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/demos/supabase-todolist-new-attachment/android/app/src/profile/AndroidManifest.xml b/demos/supabase-todolist-new-attachment/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/demos/supabase-todolist-new-attachment/android/build.gradle.kts b/demos/supabase-todolist-new-attachment/android/build.gradle.kts new file mode 100644 index 00000000..89176ef4 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/android/build.gradle.kts @@ -0,0 +1,21 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/demos/supabase-todolist-new-attachment/android/gradle.properties b/demos/supabase-todolist-new-attachment/android/gradle.properties new file mode 100644 index 00000000..f018a618 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/demos/supabase-todolist-new-attachment/android/gradle/wrapper/gradle-wrapper.properties b/demos/supabase-todolist-new-attachment/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..ac3b4792 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/demos/supabase-todolist-new-attachment/android/settings.gradle.kts b/demos/supabase-todolist-new-attachment/android/settings.gradle.kts new file mode 100644 index 00000000..ab39a10a --- /dev/null +++ b/demos/supabase-todolist-new-attachment/android/settings.gradle.kts @@ -0,0 +1,25 @@ +pluginManagement { + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.7.3" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false +} + +include(":app") diff --git a/demos/supabase-todolist-new-attachment/database.sql b/demos/supabase-todolist-new-attachment/database.sql new file mode 100644 index 00000000..eecc9768 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/database.sql @@ -0,0 +1,71 @@ +-- Create tables +create table + public.lists ( + id uuid not null default gen_random_uuid (), + created_at timestamp with time zone not null default now(), + name text not null, + owner_id uuid not null, + constraint lists_pkey primary key (id), + constraint lists_owner_id_fkey foreign key (owner_id) references auth.users (id) on delete cascade + ) tablespace pg_default; + +create table + public.todos ( + id uuid not null default gen_random_uuid (), + created_at timestamp with time zone not null default now(), + completed_at timestamp with time zone null, + description text not null, + completed boolean not null default false, + created_by uuid null, + completed_by uuid null, + list_id uuid not null, + photo_id uuid null, + constraint todos_pkey primary key (id), + constraint todos_created_by_fkey foreign key (created_by) references auth.users (id) on delete set null, + constraint todos_completed_by_fkey foreign key (completed_by) references auth.users (id) on delete set null, + constraint todos_list_id_fkey foreign key (list_id) references lists (id) on delete cascade + ) tablespace pg_default; + +-- Create publication for powersync +create publication powersync for table lists, todos; + +-- Set up Row Level Security (RLS) +-- See https://supabase.com/docs/guides/auth/row-level-security for more details. +alter table public.lists + enable row level security; + +alter table public.todos + enable row level security; + +create policy "owned lists" on public.lists for ALL using ( + auth.uid() = owner_id +); + +create policy "todos in owned lists" on public.todos for ALL using ( + auth.uid() IN ( + SELECT lists.owner_id FROM lists WHERE (lists.id = todos.list_id) + ) +); + +-- This trigger automatically creates some sample data when a user registers. +-- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details. +create function public.handle_new_user_sample_data() +returns trigger as $$ +declare + new_list_id uuid; +begin + insert into public.lists (name, owner_id) + values ('Shopping list', new.id) + returning id into new_list_id; + + insert into public.todos(description, list_id, created_by) + values ('Bread', new_list_id, new.id); + + insert into public.todos(description, list_id, created_by) + values ('Apples', new_list_id, new.id); + + return new; +end; +$$ language plpgsql security definer; + +create trigger new_user_sample_data after insert on auth.users for each row execute procedure public.handle_new_user_sample_data(); diff --git a/demos/supabase-todolist-new-attachment/flutter_01.png b/demos/supabase-todolist-new-attachment/flutter_01.png new file mode 100644 index 0000000000000000000000000000000000000000..595f48580be688d7657ff95f92e4f05172ff3eac GIT binary patch literal 73026 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 literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/ios/.gitignore b/demos/supabase-todolist-new-attachment/ios/.gitignore new file mode 100644 index 00000000..7a7f9873 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/demos/supabase-todolist-new-attachment/ios/Flutter/AppFrameworkInfo.plist b/demos/supabase-todolist-new-attachment/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 00000000..7c569640 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/demos/supabase-todolist-new-attachment/ios/Flutter/Debug.xcconfig b/demos/supabase-todolist-new-attachment/ios/Flutter/Debug.xcconfig new file mode 100644 index 00000000..ec97fc6f --- /dev/null +++ b/demos/supabase-todolist-new-attachment/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/demos/supabase-todolist-new-attachment/ios/Flutter/Release.xcconfig b/demos/supabase-todolist-new-attachment/ios/Flutter/Release.xcconfig new file mode 100644 index 00000000..c4855bfe --- /dev/null +++ b/demos/supabase-todolist-new-attachment/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/demos/supabase-todolist-new-attachment/ios/Podfile b/demos/supabase-todolist-new-attachment/ios/Podfile new file mode 100644 index 00000000..e9f73048 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/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/supabase-todolist-new-attachment/ios/Podfile.lock b/demos/supabase-todolist-new-attachment/ios/Podfile.lock new file mode 100644 index 00000000..aac7e03f --- /dev/null +++ b/demos/supabase-todolist-new-attachment/ios/Podfile.lock @@ -0,0 +1,89 @@ +PODS: + - app_links (0.0.2): + - Flutter + - camera_avfoundation (0.0.1): + - Flutter + - Flutter (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - powersync-sqlite-core (0.4.2) + - powersync_flutter_libs (0.0.1): + - Flutter + - powersync-sqlite-core (~> 0.4.2) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (3.49.2): + - sqlite3/common (= 3.49.2) + - sqlite3/common (3.49.2) + - sqlite3/dbstatvtab (3.49.2): + - sqlite3/common + - sqlite3/fts5 (3.49.2): + - sqlite3/common + - sqlite3/math (3.49.2): + - sqlite3/common + - sqlite3/perf-threadsafe (3.49.2): + - sqlite3/common + - sqlite3/rtree (3.49.2): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (~> 3.49.1) + - sqlite3/dbstatvtab + - sqlite3/fts5 + - sqlite3/math + - sqlite3/perf-threadsafe + - sqlite3/rtree + - url_launcher_ios (0.0.1): + - Flutter + +DEPENDENCIES: + - app_links (from `.symlinks/plugins/app_links/ios`) + - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) + - Flutter (from `Flutter`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - powersync_flutter_libs (from `.symlinks/plugins/powersync_flutter_libs/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + +SPEC REPOS: + trunk: + - powersync-sqlite-core + - sqlite3 + +EXTERNAL SOURCES: + app_links: + :path: ".symlinks/plugins/app_links/ios" + camera_avfoundation: + :path: ".symlinks/plugins/camera_avfoundation/ios" + Flutter: + :path: Flutter + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + powersync_flutter_libs: + :path: ".symlinks/plugins/powersync_flutter_libs/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + sqlite3_flutter_libs: + :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + +SPEC CHECKSUMS: + app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7 + camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + powersync-sqlite-core: a58efd88833861f0a8bb636c171bdf0ed55c9801 + powersync_flutter_libs: 881187a07f70ecabaf802fce45b186485464d618 + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 + sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + +PODFILE CHECKSUM: f7b3cb7384a2d5da4b22b090e1f632de7f377987 + +COCOAPODS: 1.16.2 diff --git a/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.pbxproj b/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..e32e9fa6 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/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 */; }; + FE22D026B50D91C63EC1E548 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3246A2C54ACF47297A0D9A97 /* 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 = ""; }; + 1AC6D6834A180EC866A1A907 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 3246A2C54ACF47297A0D9A97 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7F544BD3701C5CF77F2FF87F /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; + F866E203CA1A8E0C6D9ABA5C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FE22D026B50D91C63EC1E548 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 114FDDDC75E03531AE956759 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 3246A2C54ACF47297A0D9A97 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 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 */, + 114FDDDC75E03531AE956759 /* 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 = ( + F866E203CA1A8E0C6D9ABA5C /* Pods-Runner.debug.xcconfig */, + 1AC6D6834A180EC866A1A907 /* Pods-Runner.release.xcconfig */, + 7F544BD3701C5CF77F2FF87F /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 39B3199DCFC6D38A9384399C /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + FE8EB41334261949D37FC328 /* [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 */ + 39B3199DCFC6D38A9384399C /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + 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"; + }; + FE8EB41334261949D37FC328 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 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/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..9c12df59 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/supabase-todolist-new-attachment/ios/Runner.xcworkspace/contents.xcworkspacedata b/demos/supabase-todolist-new-attachment/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/demos/supabase-todolist-new-attachment/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demos/supabase-todolist-new-attachment/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/demos/supabase-todolist-new-attachment/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/demos/supabase-todolist-new-attachment/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/AppDelegate.swift b/demos/supabase-todolist-new-attachment/ios/Runner/AppDelegate.swift new file mode 100644 index 00000000..b6363034 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d36b1fab --- /dev/null +++ b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc9ada4725e9b0ddb1deab583e5b5102493aa332 GIT binary patch literal 10932 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 literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..797d452e458972bab9d994556c8305db4c827017 GIT binary patch literal 406 zcmV;H0crk;P))>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed2d933e1120817fe9182483a228007b18ab6ae GIT binary patch literal 450 zcmV;z0X_bSP)iGWQ_5NJQ_~rNh*z)}eT%KUb z`7gNk0#AwF^#0T0?hIa^`~Ck;!}#m+_uT050aTR(J!bU#|IzRL%^UsMS#KsYnTF*!YeDOytlP4VhV?b} z%rz_<=#CPc)tU1MZTq~*2=8~iZ!lSa<{9b@2Jl;?IEV8)=fG217*|@)CCYgFze-x? zIFODUIA>nWKpE+bn~n7;-89sa>#DR>TSlqWk*!2hSN6D~Qb#VqbP~4Fk&m`@1$JGr zXPIdeRE&b2Thd#{MtDK$px*d3-Wx``>!oimf%|A-&-q*6KAH)e$3|6JV%HX{Hig)k suLT-RhftRq8b9;(V=235Wa|I=027H2wCDra;{X5v07*qoM6N<$f;9x^2LJ#7 literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd7b0099ca80c806f8fe495613e8d6c69460d76 GIT binary patch literal 282 zcmV+#0p(^bcu7P-R4C8Q z&e;xxFbF_Vrezo%_kH*OKhshZ6BFpG-Y1e10`QXJKbND7AMQ&cMj60B5TNObaZxYybcN07*qoM6N<$g3m;S%K!iX literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fe730945a01f64a61e2235dbe3f45b08f7729182 GIT binary patch literal 462 zcmV;<0WtoGP)-}iV`2<;=$?g5M=KQbZ{F&YRNy7Nn@%_*5{gvDM0aKI4?ESmw z{NnZg)A0R`+4?NF_RZexyVB&^^ZvN!{I28tr{Vje;QNTz`dG&Jz0~Ek&f2;*Z7>B|cg}xYpxEFY+0YrKLF;^Q+-HreN0P{&i zK~zY`?b7ECf-n?@;d<&orQ*Q7KoR%4|C>{W^h6@&01>0SKS`dn{Q}GT%Qj_{PLZ_& zs`MFI#j-(>?bvdZ!8^xTwlY{qA)T4QLbY@j(!YJ7aXJervHy6HaG_2SB`6CC{He}f zHVw(fJWApwPq!6VY7r1w-Fs)@ox~N+q|w~e;JI~C4Vf^@d>Wvj=fl`^u9x9wd9 zR%3*Q+)t%S!MU_`id^@&Y{y7-r98lZX0?YrHlfmwb?#}^1b{8g&KzmkE(L>Z&)179 zp<)v6Y}pRl100G2FL_t(o!|l{-Q-VMg#&MKg7c{O0 z2wJImOS3Gy*Z2Qifdv~JYOp;v+U)a|nLoc7hNH;I$;lzDt$}rkaFw1mYK5_0Q(Sut zvbEloxON7$+HSOgC9Z8ltuC&0OSF!-mXv5caV>#bc3@hBPX@I$58-z}(ZZE!t-aOG zpjNkbau@>yEzH(5Yj4kZiMH32XI!4~gVXNnjAvRx;Sdg^`>2DpUEwoMhTs_st8pKG z(%SHyHdU&v%f36~uERh!bd`!T2dw;z6PrOTQ7Vt*#9F2uHlUVnb#ev_o^fh}Dzmq} zWtlk35}k=?xj28uO|5>>$yXadTUE@@IPpgH`gJ~Ro4>jd1IF|(+IX>8M4Ps{PNvmI zNj4D+XgN83gPt_Gm}`Ybv{;+&yu-C(Grdiahmo~BjG-l&mWM+{e5M1sm&=xduwgM9 z`8OEh`=F3r`^E{n_;%9weN{cf2%7=VzC@cYj+lg>+3|D|_1C@{hcU(DyQG_BvBWe? zvTv``=%b1zrol#=R`JB)>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..502f463a9bc882b461c96aadf492d1729e49e725 GIT binary patch literal 586 zcmV-Q0=4~#P)+}#`wDE{8-2Mebf5<{{PqV{TgVcv*r8?UZ3{-|G?_}T*&y;@cqf{ z{Q*~+qr%%p!1pS*_Uicl#q9lc(D`!D`LN62sNwq{oYw(Wmhk)k<@f$!$@ng~_5)Ru z0Z)trIA5^j{DIW^c+vT2%lW+2<(RtE2wR;4O@)Tm`Xr*?A(qYoM}7i5Yxw>D(&6ou zxz!_Xr~yNF+waPe00049Nkl*;a!v6h%{rlvIH#gW3s8p;bFr=l}mRqpW2h zw=OA%hdyL~z+UHOzl0eKhEr$YYOL-c-%Y<)=j?(bzDweB7{b+%_ypvm_cG{SvM=DK zhv{K@m>#Bw>2W$eUI#iU)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G! zhkE!s;%oku3;IwG3U^2kw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn`0X*2 zy3(k600_CSZj?O$Qu%&$;|TGUJrptR(HzyIx>5E(2r{eA(<6t3e3I0B)7d6s7?Z5J zZ!rtKvA{MiEBm&KFtoifx>5P^Z=vl)95XJn()aS5%ad(s?4-=Tkis9IGu{`Fy8r+H07*qoM6N<$f20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec303439225b78712f49115768196d8d76f6790 GIT binary patch literal 862 zcmV-k1EKthP)20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f5fea27c705180eb716271f41b582e76dcbd90 GIT binary patch literal 1674 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 literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0467bf12aa4d28f374bb26596605a46dcbb3e7c8 GIT binary patch literal 1418 zcmV;51$Fv~P)q zKfU)WzW*n(@|xWGCA9ScMt*e9`2kdxPQ&&>|-UCa7_51w+ zLUsW@ZzZSW0y$)Hp~e9%PvP|a03ks1`~K?q{u;6NC8*{AOqIUq{CL&;p56Lf$oQGq z^={4hPQv)y=I|4n+?>7Fim=dxt1 z2H+Dm+1+fh+IF>G0SjJMkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJT zkdTm&kdTm&kdTm&kdP`esgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>Tmf` zm1{eLgw!b{bXkjWbF%dTkTZEJWyWOb##Lfw4EK2}<0d6%>AGS{po>WCOy&f$Tay_> z?NBlkpo@s-O;0V%Y_Xa-G#_O08q5LR*~F%&)}{}r&L%Sbs8AS4t7Y0NEx*{soY=0MZExqA5XHQkqi#4gW3 zqODM^iyZl;dvf)-bOXtOru(s)Uc7~BFx{w-FK;2{`VA?(g&@3z&bfLFyctOH!cVsF z7IL=fo-qBndRUm;kAdXR4e6>k-z|21AaN%ubeVrHl*<|s&Ax@W-t?LR(P-24A5=>a z*R9#QvjzF8n%@1Nw@?CG@6(%>+-0ASK~jEmCV|&a*7-GKT72W<(TbSjf)&Eme6nGE z>Gkj4Sq&2e+-G%|+NM8OOm5zVl9{Z8Dd8A5z3y8mZ=4Bv4%>as_{9cN#bm~;h>62( zdqY93Zy}v&c4n($Vv!UybR8ocs7#zbfX1IY-*w~)p}XyZ-SFC~4w>BvMVr`dFbelV{lLL0bx7@*ZZdebr3`sP;? zVImji)kG)(6Juv0lz@q`F!k1FE;CQ(D0iG$wchPbKZQELlsZ#~rt8#90Y_Xh&3U-< z{s<&cCV_1`^TD^ia9!*mQDq& zn2{r`j};V|uV%_wsP!zB?m%;FeaRe+X47K0e+KE!8C{gAWF8)lCd1u1%~|M!XNRvw zvtqy3iz0WSpWdhn6$hP8PaRBmp)q`#PCA`Vd#Tc$@f1tAcM>f_I@bC)hkI9|o(Iqv zo}Piadq!j76}004RBio<`)70k^`K1NK)q>w?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF z34$0Z;QO!JOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUC YUoZo%k(ykuW&i*H07*qoM6N<$f+CH{y8r+H literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 00000000..0bedcf2f --- /dev/null +++ b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 00000000..89c2725b --- /dev/null +++ b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Base.lproj/LaunchScreen.storyboard b/demos/supabase-todolist-new-attachment/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..f2e259c7 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Base.lproj/Main.storyboard b/demos/supabase-todolist-new-attachment/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f3c28516 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Info.plist b/demos/supabase-todolist-new-attachment/ios/Runner/Info.plist new file mode 100644 index 00000000..fc3bb480 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/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/supabase-todolist-new-attachment/ios/Runner/Runner-Bridging-Header.h b/demos/supabase-todolist-new-attachment/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 00000000..308a2a56 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/demos/supabase-todolist-new-attachment/ios/RunnerTests/RunnerTests.swift b/demos/supabase-todolist-new-attachment/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..86a7c3b1 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/demos/supabase-todolist-new-attachment/lib/app_config_template.dart b/demos/supabase-todolist-new-attachment/lib/app_config_template.dart new file mode 100644 index 00000000..ccfdfa21 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/lib/app_config_template.dart @@ -0,0 +1,9 @@ +// Copy this template: `cp lib/app_config_template.dart lib/app_config.dart` +// Edit lib/app_config.dart and enter your Supabase and PowerSync project details. +class AppConfig { + static const String supabaseUrl = 'https://foo.supabase.co'; + static const String supabaseAnonKey = 'foo'; + static const String powersyncUrl = 'https://foo.powersync.journeyapps.com'; + static const String supabaseStorageBucket = + ''; // Optional. Only required when syncing attachments and using Supabase Storage. See packages/powersync_attachments_helper. +} diff --git a/demos/supabase-todolist-new-attachment/lib/attachments/camera_helpers.dart b/demos/supabase-todolist-new-attachment/lib/attachments/camera_helpers.dart new file mode 100644 index 00000000..cf2a2cf5 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/lib/attachments/camera_helpers.dart @@ -0,0 +1,20 @@ +import 'package:camera/camera.dart'; +import 'package:flutter/widgets.dart'; +import 'package:powersync_flutter_demo_new/powersync.dart'; + +Future setupCamera() async { + // Ensure that plugin services are initialized so that `availableCameras()` + // can be called before `runApp()` + WidgetsFlutterBinding.ensureInitialized(); + // Obtain a list of the available cameras on the device. + try { + final cameras = await availableCameras(); + // Get a specific camera from the list of available cameras. + final camera = cameras.isNotEmpty ? cameras.first : null; + return camera; + } catch (e) { + // Camera is not supported on all platforms + log.warning('Failed to setup camera: $e'); + return null; + } +} diff --git a/demos/supabase-todolist-new-attachment/lib/attachments/photo_capture_widget.dart b/demos/supabase-todolist-new-attachment/lib/attachments/photo_capture_widget.dart new file mode 100644 index 00000000..90c06944 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/lib/attachments/photo_capture_widget.dart @@ -0,0 +1,96 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:powersync/powersync.dart' as powersync; +import 'package:powersync_flutter_demo_new/attachments/queue.dart'; +import 'package:powersync_flutter_demo_new/models/todo_item.dart'; +import 'package:powersync_flutter_demo_new/powersync.dart'; + +class TakePhotoWidget extends StatefulWidget { + final String todoId; + final CameraDescription camera; + + const TakePhotoWidget({super.key, required this.todoId, required this.camera}); + + @override + State createState() { + return _TakePhotoWidgetState(); + } +} + +class _TakePhotoWidgetState extends State { + late CameraController _cameraController; + late Future _initializeControllerFuture; + final log = Logger('TakePhotoWidget'); + + @override + void initState() { + super.initState(); + + _cameraController = CameraController( + widget.camera, + ResolutionPreset.medium, + ); + + _initializeControllerFuture = _cameraController.initialize(); + } + + @override + void dispose() { + _cameraController.dispose(); + super.dispose(); + } + + Future _takePhoto(context) async { + try { + log.info('Taking photo for todo: ${widget.todoId}'); + await _initializeControllerFuture; + final XFile photo = await _cameraController.takePicture(); + + // Read the photo data as a stream + final photoFile = File(photo.path); + if (!await photoFile.exists()) { + log.warning('Photo file does not exist: ${photo.path}'); + return; + } + + final photoDataStream = photoFile.openRead().cast(); + + // Save the photo attachment directly with the data stream + final attachment = await savePhotoAttachment(photoDataStream, widget.todoId); + + log.info('Photo attachment saved with ID: ${attachment.id}'); + } catch (e) { + log.severe('Error taking photo: $e'); + } + Navigator.pop(context); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FutureBuilder( + future: _initializeControllerFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return CameraPreview(_cameraController); + } else { + return const CircularProgressIndicator(); + } + }, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => _takePhoto(context), + child: const Text('Take Photo'), + ), + ], + ); + } +} diff --git a/demos/supabase-todolist-new-attachment/lib/attachments/photo_widget.dart b/demos/supabase-todolist-new-attachment/lib/attachments/photo_widget.dart new file mode 100644 index 00000000..fd0029e8 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/lib/attachments/photo_widget.dart @@ -0,0 +1,152 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:powersync_attachments_stream/powersync_attachments_stream.dart'; +import 'package:powersync_flutter_demo_new/attachments/camera_helpers.dart'; +import 'package:powersync_flutter_demo_new/attachments/photo_capture_widget.dart'; +import 'package:powersync_flutter_demo_new/attachments/queue.dart'; + +import '../models/todo_item.dart'; + +class PhotoWidget extends StatefulWidget { + final TodoItem todo; + + PhotoWidget({ + required this.todo, + }) : super(key: ObjectKey(todo.id)); + + @override + State createState() { + return _PhotoWidgetState(); + } +} + +class _ResolvedPhotoState { + String? photoPath; + bool fileExists; + Attachment? attachment; + + _ResolvedPhotoState( + {required this.photoPath, required this.fileExists, this.attachment}); +} + +class _PhotoWidgetState extends State { + late String photoPath; + final log = Logger('PhotoWidget'); + + Future<_ResolvedPhotoState> _getPhotoState(photoId) async { + log.info('getPhotoState: $photoId'); + if (photoId == null) { + return _ResolvedPhotoState(photoPath: null, fileExists: false); + } + + photoPath = attachmentQueue.getLocalUri('$photoId.jpg'); + + bool fileExists = await File(photoPath).exists(); + + log.info('fileExists: $fileExists'); + + final row = await attachmentQueue.db + .getOptional('SELECT * FROM attachments_queue WHERE id = ?', [photoId]); + + log.info('row: $row'); + + if (row != null) { + Attachment attachment = Attachment.fromRow(row); + // Attachment attachment = Attachment.fromMap(row); + + // Log as JSON using logging package + // ignore: avoid_print + attachmentQueue.logger.info({ + 'event': 'attachment_object', + 'photoPath': photoPath, + 'fileExists': fileExists, + 'attachment': '', + }); + + return _ResolvedPhotoState( + photoPath: photoPath, fileExists: fileExists, attachment: attachment); + } + + return _ResolvedPhotoState( + photoPath: photoPath, fileExists: fileExists, attachment: null); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _getPhotoState(widget.todo.photoId), + builder: (BuildContext context, + AsyncSnapshot<_ResolvedPhotoState> snapshot) { + if (snapshot.data == null) { + return Container(); + } + final data = snapshot.data!; + Widget takePhotoButton = ElevatedButton( + onPressed: () async { + final camera = await setupCamera(); + if (!context.mounted) return; + + if (camera == null) { + const snackBar = SnackBar( + content: Text('No camera available'), + backgroundColor: + Colors.red, // Optional: to highlight it's an error + ); + + ScaffoldMessenger.of(context).showSnackBar(snackBar); + return; + } + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + TakePhotoWidget(todoId: widget.todo.id, camera: camera), + ), + ); + }, + child: const Text('Take Photo'), + ); + + if (widget.todo.photoId == null) { + return takePhotoButton; + } + + String? filePath = data.photoPath; + bool fileIsDownloading = !data.fileExists; + bool fileArchived = + data.attachment?.state == AttachmentState.archived; + + if (fileArchived) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text("Unavailable"), + const SizedBox(height: 8), + takePhotoButton + ], + ); + } + + if (fileIsDownloading) { + return const Text("Downloading..."); + } + + File imageFile = File(filePath!); + int lastModified = imageFile.existsSync() + ? imageFile.lastModifiedSync().millisecondsSinceEpoch + : 0; + Key key = ObjectKey('$filePath:$lastModified'); + + return Image.file( + key: key, + imageFile, + width: 50, + height: 50, + ); + }); + } +} diff --git a/demos/supabase-todolist-new-attachment/lib/attachments/queue.dart b/demos/supabase-todolist-new-attachment/lib/attachments/queue.dart new file mode 100644 index 00000000..52810c7a --- /dev/null +++ b/demos/supabase-todolist-new-attachment/lib/attachments/queue.dart @@ -0,0 +1,74 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:logging/logging.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:powersync/powersync.dart'; +import 'package:powersync_attachments_stream/powersync_attachments_stream.dart'; +import 'package:powersync_flutter_demo_new/attachments/remote_storage_adapter.dart'; + +late AttachmentQueue attachmentQueue; +final remoteStorage = SupabaseStorageAdapter(); +final log = Logger('AttachmentQueue'); + +Future onDownloadError(Attachment attachment, Object exception) async { + if (exception.toString().contains('Object not found')) { + return false; + } + return true; +} + +Future initializeAttachmentQueue(PowerSyncDatabase db) async { + // Use the app's document directory for local storage + final Directory appDocDir = await getApplicationDocumentsDirectory(); + final localStorage = IOLocalStorage(appDocDir); + + log.info('directory: ${appDocDir.path}'); + log.info('localStorage: $localStorage'); + + attachmentQueue = AttachmentQueue( + db: db, + remoteStorage: remoteStorage, + attachmentsDirectory: '${appDocDir.path}/attachments', + watchAttachments: () => db.watch(''' + SELECT photo_id as id FROM todos WHERE photo_id IS NOT NULL + ''').map((results) { + final items = results.map((row) => WatchedAttachmentItem(id: row['id'] as String, fileExtension: 'jpg')).toList(); + log.info('Watched attachment IDs: ${items.map((e) => e.id).toList()}'); + return items; + }), + localStorage: localStorage, + errorHandler: null, // Optionally implement SyncErrorHandler + ); + // await attachmentQueue.startSync(); +} + +Future savePhotoAttachment(Stream photoData, String todoId, + {String mediaType = 'image/jpeg'}) async { + log.info('Saving photo attachment for todo: $todoId'); + + // Save the file using the AttachmentQueue API + return await attachmentQueue.saveFile( + data: photoData, + mediaType: mediaType, + fileExtension: 'jpg', + metaData: 'Photo attachment for todo: $todoId', + updateHook: (context, attachment) async { + // Update the todo item to reference this attachment + await context.execute( + 'UPDATE todos SET photo_id = ? WHERE id = ?', + [attachment.id, todoId], + ); + }, + ); +} + +Future deletePhotoAttachment(String fileId) async { + log.info('deletePhotoAttachment: $fileId'); + return await attachmentQueue.deleteFile( + attachmentId: fileId, + updateHook: (context, attachment) async { + // Optionally update relationships in the same transaction + }, + ); +} diff --git a/demos/supabase-todolist-new-attachment/lib/attachments/remote_storage_adapter.dart b/demos/supabase-todolist-new-attachment/lib/attachments/remote_storage_adapter.dart new file mode 100644 index 00000000..a3e49d86 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/lib/attachments/remote_storage_adapter.dart @@ -0,0 +1,74 @@ +import 'dart:io'; +import 'dart:typed_data'; +import 'package:powersync_attachments_stream/powersync_attachments_stream.dart'; +import 'package:powersync_flutter_demo_new/app_config.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:image/image.dart' as img; + +class SupabaseStorageAdapter implements RemoteStorage { + @override + Future uploadFile( + Stream> fileData, Attachment attachment) async { + _checkSupabaseBucketIsConfigured(); + final tempFile = + File('${Directory.systemTemp.path}/${attachment.filename}'); + final sink = tempFile.openWrite(); + await for (final chunk in fileData) { + sink.add(chunk); + } + await sink.close(); + print('uploadFile: ${attachment.filename}'); + try { + await Supabase.instance.client.storage + .from(AppConfig.supabaseStorageBucket) + .upload(attachment.filename, tempFile, + fileOptions: FileOptions( + contentType: + attachment.mediaType ?? 'application/octet-stream')); + print('uploadFile: ${attachment.filename}'); + } catch (error) { + throw Exception(error); + } finally { + if (await tempFile.exists()) { + await tempFile.delete(); + } + } + } + + @override + Future>> downloadFile(Attachment attachment) async { + _checkSupabaseBucketIsConfigured(); + print('downloadFile: ${attachment.filename}'); + try { + Uint8List fileBlob = await Supabase.instance.client.storage + .from(AppConfig.supabaseStorageBucket) + .download(attachment.filename); + final image = img.decodeImage(fileBlob); + Uint8List blob = img.JpegEncoder().encode(image!); + print('downloadFile: ${blob.length}'); + return Stream.value(blob); + } catch (error) { + throw Exception(error); + } + } + + @override + Future deleteFile(Attachment attachment) async { + print('deleteFile: ${attachment.filename}'); + _checkSupabaseBucketIsConfigured(); + try { + await Supabase.instance.client.storage + .from(AppConfig.supabaseStorageBucket) + .remove([attachment.filename]); + } catch (error) { + throw Exception(error); + } + } + + void _checkSupabaseBucketIsConfigured() { + if (AppConfig.supabaseStorageBucket.isEmpty) { + throw Exception( + 'Supabase storage bucket is not configured in app_config.dart'); + } + } +} diff --git a/demos/supabase-todolist-new-attachment/lib/fts_helpers.dart b/demos/supabase-todolist-new-attachment/lib/fts_helpers.dart new file mode 100644 index 00000000..cbad8480 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/lib/fts_helpers.dart @@ -0,0 +1,17 @@ +import 'package:powersync_flutter_demo_new/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/supabase-todolist-new-attachment/lib/main.dart b/demos/supabase-todolist-new-attachment/lib/main.dart new file mode 100644 index 00000000..2e3eeea9 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/lib/main.dart @@ -0,0 +1,132 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:powersync_flutter_demo_new/app_config.dart'; +import 'package:powersync_flutter_demo_new/attachments/queue.dart'; +import 'package:powersync_flutter_demo_new/models/schema.dart'; + +import './powersync.dart'; +import './widgets/lists_page.dart'; +import './widgets/login_page.dart'; +import './widgets/query_widget.dart'; +import './widgets/signup_page.dart'; +import './widgets/status_app_bar.dart'; + +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(); + + if (AppConfig.supabaseStorageBucket.isNotEmpty) { + await initializeAttachmentQueue(db); + attachmentQueue.startSync(); + } + + final loggedIn = isLoggedIn(); + runApp(MyApp(loggedIn: loggedIn)); +} + +const defaultQuery = 'SELECT * from $todosTable'; + +const listsPage = ListsPage(); +const homePage = listsPage; + +const sqlConsolePage = Scaffold( + appBar: StatusAppBar(title: Text('SQL Console')), + body: QueryWidget(defaultQuery: defaultQuery)); + +const loginPage = LoginPage(); + +const signupPage = SignupPage(); + +class MyApp extends StatelessWidget { + final bool loggedIn; + + const MyApp({super.key, required this.loggedIn}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'PowerSync Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: loggedIn ? homePage : loginPage); + } +} + +class MyHomePage extends StatelessWidget { + const MyHomePage( + {super.key, + required this.title, + required this.content, + this.floatingActionButton}); + + final String title; + final Widget content; + final Widget? floatingActionButton; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: StatusAppBar(title: Text(title)), + body: Center(child: content), + floatingActionButton: floatingActionButton, + drawer: Drawer( + // Add a ListView to the drawer. This ensures the user can scroll + // through the options in the drawer if there isn't enough vertical + // space to fit everything. + child: ListView( + // Important: Remove any padding from the ListView. + padding: EdgeInsets.zero, + children: [ + const DrawerHeader( + decoration: BoxDecoration( + color: Colors.blue, + ), + child: Text(''), + ), + ListTile( + title: const Text('SQL Console'), + onTap: () { + var navigator = Navigator.of(context); + navigator.pop(); + + navigator.push(MaterialPageRoute( + builder: (context) => sqlConsolePage, + )); + }, + ), + ListTile( + title: const Text('Sign Out'), + onTap: () async { + var navigator = Navigator.of(context); + navigator.pop(); + await logout(); + + navigator.pushReplacement(MaterialPageRoute( + builder: (context) => loginPage, + )); + }, + ), + ], + ), + ), + ); + } +} diff --git a/demos/supabase-todolist-new-attachment/lib/migrations/fts_setup.dart b/demos/supabase-todolist-new-attachment/lib/migrations/fts_setup.dart new file mode 100644 index 00000000..9754e44e --- /dev/null +++ b/demos/supabase-todolist-new-attachment/lib/migrations/fts_setup.dart @@ -0,0 +1,76 @@ +import 'package:powersync/powersync.dart'; +import 'package:powersync/sqlite_async.dart'; + +import 'helpers.dart'; +import '../models/schema.dart'; + +final migrations = SqliteMigrations(); + +/// Create a Full Text Search table for the given table and columns +/// with an option to use a different tokenizer otherwise it defaults +/// to unicode61. It also creates the triggers that keep the FTS table +/// and the PowerSync table in sync. +SqliteMigration createFtsMigration( + {required int migrationVersion, + required String tableName, + required List columns, + String tokenizationMethod = 'unicode61'}) { + String internalName = + schema.tables.firstWhere((table) => table.name == tableName).internalName; + String stringColumns = columns.join(', '); + + return SqliteMigration(migrationVersion, (tx) async { + // Add FTS table + await tx.execute(''' + CREATE VIRTUAL TABLE IF NOT EXISTS fts_$tableName + USING fts5(id UNINDEXED, $stringColumns, tokenize='$tokenizationMethod'); + '''); + // Copy over records already in table + await tx.execute(''' + INSERT INTO fts_$tableName(rowid, id, $stringColumns) + SELECT rowid, id, ${generateJsonExtracts(ExtractType.columnOnly, 'data', columns)} FROM $internalName; + '''); + // Add INSERT, UPDATE and DELETE and triggers to keep fts table in sync with table + await tx.execute(''' + CREATE TRIGGER IF NOT EXISTS fts_insert_trigger_$tableName AFTER INSERT ON $internalName + BEGIN + INSERT INTO fts_$tableName(rowid, id, $stringColumns) + VALUES ( + NEW.rowid, + NEW.id, + ${generateJsonExtracts(ExtractType.columnOnly, 'NEW.data', columns)} + ); + END; + '''); + await tx.execute(''' + CREATE TRIGGER IF NOT EXISTS fts_update_trigger_$tableName AFTER UPDATE ON $internalName BEGIN + UPDATE fts_$tableName + SET ${generateJsonExtracts(ExtractType.columnInOperation, 'NEW.data', columns)} + WHERE rowid = NEW.rowid; + END; + '''); + await tx.execute(''' + CREATE TRIGGER IF NOT EXISTS fts_delete_trigger_$tableName AFTER DELETE ON $internalName BEGIN + DELETE FROM fts_$tableName WHERE rowid = OLD.rowid; + END; + '''); + }); +} + +/// This is where you can add more migrations to generate FTS tables +/// that correspond to the tables in your schema and populate them +/// with the data you would like to search on +Future configureFts(PowerSyncDatabase db) async { + migrations + ..add(createFtsMigration( + migrationVersion: 1, + tableName: 'lists', + columns: ['name'], + tokenizationMethod: 'porter unicode61')) + ..add(createFtsMigration( + migrationVersion: 2, + tableName: 'todos', + columns: ['description', 'list_id'], + )); + await migrations.migrate(db); +} diff --git a/demos/supabase-todolist-new-attachment/lib/migrations/helpers.dart b/demos/supabase-todolist-new-attachment/lib/migrations/helpers.dart new file mode 100644 index 00000000..c1a779e1 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/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/supabase-todolist-new-attachment/lib/models/schema.dart b/demos/supabase-todolist-new-attachment/lib/models/schema.dart new file mode 100644 index 00000000..0ca9722d --- /dev/null +++ b/demos/supabase-todolist-new-attachment/lib/models/schema.dart @@ -0,0 +1,27 @@ +import 'package:powersync/powersync.dart'; +import 'package:powersync_attachments_stream/powersync_attachments_stream.dart'; + +const todosTable = 'todos'; + +Schema schema = Schema(([ + const Table(todosTable, [ + Column.text('list_id'), + Column.text('photo_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')]) + ]), + const Table('lists', [ + Column.text('created_at'), + Column.text('name'), + Column.text('owner_id') + ]), + AttachmentsQueueTable( + attachmentsQueueTableName: defaultAttachmentsQueueTableName) +])); diff --git a/demos/supabase-todolist-new-attachment/lib/models/todo_item.dart b/demos/supabase-todolist-new-attachment/lib/models/todo_item.dart new file mode 100644 index 00000000..5eb1de99 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/lib/models/todo_item.dart @@ -0,0 +1,51 @@ +import 'package:powersync_flutter_demo_new/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 = ?', + [getUserId(), id]); + } + } + + Future delete() async { + await db.execute('DELETE FROM $todosTable WHERE id = ?', [id]); + } + + static Future addPhoto(String photoId, String id) async { + print('addPhoto: $photoId, $id'); + await db.execute( + 'UPDATE $todosTable SET photo_id = ? WHERE id = ?', [photoId, id]); + } +} diff --git a/demos/supabase-todolist-new-attachment/lib/models/todo_list.dart b/demos/supabase-todolist-new-attachment/lib/models/todo_list.dart new file mode 100644 index 00000000..86170e02 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/lib/models/todo_list.dart @@ -0,0 +1,108 @@ +import 'package:powersync/powersync.dart'; +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); + }); + } + + static Stream watchSyncStatus() { + return db.statusStream; + } + + /// 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, 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, getUserId()]); + return TodoItem.fromRow(results.first); + } +} diff --git a/demos/supabase-todolist-new-attachment/lib/powersync.dart b/demos/supabase-todolist-new-attachment/lib/powersync.dart new file mode 100644 index 00000000..f44aa8f4 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/lib/powersync.dart @@ -0,0 +1,200 @@ +// This file performs setup of the PowerSync database +import 'package:flutter/foundation.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:powersync/powersync.dart'; +import 'package:powersync_flutter_demo_new/migrations/fts_setup.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +import './app_config.dart'; +import './models/schema.dart'; +import './supabase.dart'; + +final log = Logger('powersync-supabase'); + +/// Postgres Response codes that we cannot recover from by retrying. +final List fatalResponseCodes = [ + // Class 22 — Data Exception + // Examples include data type mismatch. + RegExp(r'^22...$'), + // Class 23 — Integrity Constraint Violation. + // Examples include NOT NULL, FOREIGN KEY and UNIQUE violations. + RegExp(r'^23...$'), + // INSUFFICIENT PRIVILEGE - typically a row-level security violation + RegExp(r'^42501$'), +]; + +/// Use Supabase for authentication and data upload. +class SupabaseConnector extends PowerSyncBackendConnector { + Future? _refreshFuture; + + SupabaseConnector(); + + /// Get a Supabase token to authenticate against the PowerSync instance. + @override + Future fetchCredentials() async { + // Wait for pending session refresh if any + await _refreshFuture; + + // Use Supabase token for PowerSync + final session = Supabase.instance.client.auth.currentSession; + if (session == null) { + // Not logged in + return null; + } + + // Use the access token to authenticate against PowerSync + final token = session.accessToken; + + // userId and expiresAt are for debugging purposes only + final userId = session.user.id; + final expiresAt = session.expiresAt == null + ? null + : DateTime.fromMillisecondsSinceEpoch(session.expiresAt! * 1000); + return PowerSyncCredentials( + endpoint: AppConfig.powersyncUrl, + token: token, + userId: userId, + expiresAt: expiresAt); + } + + @override + void invalidateCredentials() { + // Trigger a session refresh if auth fails on PowerSync. + // Generally, sessions should be refreshed automatically by Supabase. + // However, in some cases it can be a while before the session refresh is + // retried. We attempt to trigger the refresh as soon as we get an auth + // failure on PowerSync. + // + // This could happen if the device was offline for a while and the session + // expired, and nothing else attempt to use the session it in the meantime. + // + // Timeout the refresh call to avoid waiting for long retries, + // and ignore any errors. Errors will surface as expired tokens. + _refreshFuture = Supabase.instance.client.auth + .refreshSession() + .timeout(const Duration(seconds: 5)) + .then((response) => null, onError: (error) => null); + } + + // Upload pending changes to Supabase. + @override + Future uploadData(PowerSyncDatabase database) async { + // This function is called whenever there is data to upload, whether the + // device is online or offline. + // If this call throws an error, it is retried periodically. + final transaction = await database.getNextCrudTransaction(); + if (transaction == null) { + return; + } + + final rest = Supabase.instance.client.rest; + CrudEntry? lastOp; + try { + // Note: If transactional consistency is important, use database functions + // or edge functions to process the entire transaction in a single call. + for (var op in transaction.crud) { + lastOp = op; + + final table = rest.from(op.table); + if (op.op == UpdateType.put) { + var data = Map.of(op.opData!); + data['id'] = op.id; + await table.upsert(data); + } else if (op.op == UpdateType.patch) { + await table.update(op.opData!).eq('id', op.id); + } else if (op.op == UpdateType.delete) { + await table.delete().eq('id', op.id); + } + } + + // All operations successful. + await transaction.complete(); + } on PostgrestException catch (e) { + if (e.code != null && + fatalResponseCodes.any((re) => re.hasMatch(e.code!))) { + /// Instead of blocking the queue with these errors, + /// discard the (rest of the) transaction. + /// + /// Note that these errors typically indicate a bug in the application. + /// If protecting against data loss is important, save the failing records + /// elsewhere instead of discarding, and/or notify the user. + log.severe('Data upload error - discarding $lastOp', e); + await transaction.complete(); + } else { + // Error may be retryable - e.g. network error or temporary server error. + // Throwing an error here causes this call to be retried after a delay. + rethrow; + } + } + } +} + +/// Global reference to the database +late final PowerSyncDatabase db; + +bool isLoggedIn() { + return Supabase.instance.client.auth.currentSession?.accessToken != null; +} + +/// id of the user currently logged in +String? getUserId() { + return Supabase.instance.client.auth.currentSession?.user.id; +} + +Future getDatabasePath() async { + const dbFilename = 'powersync-demo-new-attachment.db'; + // getApplicationSupportDirectory is not supported on Web + if (kIsWeb) { + return dbFilename; + } + final dir = await getApplicationSupportDirectory(); + return join(dir.path, dbFilename); +} + +const options = SyncOptions(syncImplementation: SyncClientImplementation.rust); + +Future openDatabase() async { + // Open the local database + db = PowerSyncDatabase( + schema: schema, path: await getDatabasePath(), logger: attachedLogger); + await db.initialize(); + + await loadSupabase(); + + SupabaseConnector? currentConnector; + + if (isLoggedIn()) { + // If the user is already logged in, connect immediately. + // Otherwise, connect once logged in. + currentConnector = SupabaseConnector(); + db.connect(connector: currentConnector, options: options); + } + + Supabase.instance.client.auth.onAuthStateChange.listen((data) async { + final AuthChangeEvent event = data.event; + if (event == AuthChangeEvent.signedIn) { + // Connect to PowerSync when the user is signed in + currentConnector = SupabaseConnector(); + db.connect(connector: currentConnector!, options: options); + } else if (event == AuthChangeEvent.signedOut) { + // Implicit sign out - disconnect, but don't delete data + currentConnector = null; + await db.disconnect(); + } else if (event == AuthChangeEvent.tokenRefreshed) { + // Supabase token refreshed - trigger token refresh for PowerSync. + currentConnector?.prefetchCredentials(); + } + }); + + // Demo using SQLite Full-Text Search with PowerSync. + // See https://docs.powersync.com/usage-examples/full-text-search for more details + await configureFts(db); +} + +/// Explicit sign out - clear database and log out. +Future logout() async { + await Supabase.instance.client.auth.signOut(); + await db.disconnectAndClear(); +} diff --git a/demos/supabase-todolist-new-attachment/lib/supabase.dart b/demos/supabase-todolist-new-attachment/lib/supabase.dart new file mode 100644 index 00000000..a69586e2 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/lib/supabase.dart @@ -0,0 +1,10 @@ +import 'package:supabase_flutter/supabase_flutter.dart'; + +import './app_config.dart'; + +loadSupabase() async { + await Supabase.initialize( + url: AppConfig.supabaseUrl, + anonKey: AppConfig.supabaseAnonKey, + ); +} diff --git a/demos/supabase-todolist-new-attachment/lib/widgets/fts_search_delegate.dart b/demos/supabase-todolist-new-attachment/lib/widgets/fts_search_delegate.dart new file mode 100644 index 00000000..0b8d6bf7 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/lib/widgets/fts_search_delegate.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:powersync_flutter_demo_new/fts_helpers.dart' as fts_helpers; +import 'package:powersync_flutter_demo_new/models/todo_list.dart'; + +import './todo_list_page.dart'; + +final log = Logger('powersync-supabase'); + +class FtsSearchDelegate extends SearchDelegate { + @override + List? buildActions(BuildContext context) { + return [ + IconButton( + onPressed: () { + query = ''; + }, + icon: const Icon(Icons.clear), + ), + ]; + } + + @override + Widget? buildLeading(BuildContext context) { + return IconButton( + onPressed: () { + close(context, null); + }, + icon: const Icon(Icons.arrow_back), + ); + } + + @override + Widget buildResults(BuildContext context) { + return FutureBuilder( + future: _search(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return ListView.builder( + itemBuilder: (context, index) { + return ListTile( + title: Text(snapshot.data?[index].name), + onTap: () { + close(context, null); + }, + ); + }, + itemCount: snapshot.data?.length, + ); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + ); + } + + @override + Widget buildSuggestions(BuildContext context) { + NavigatorState navigator = Navigator.of(context); + + return FutureBuilder( + future: _search(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return ListView.builder( + itemBuilder: (context, index) { + return ListTile( + title: Text(snapshot.data?[index]['name'] ?? ''), + onTap: () async { + 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 { + if (query.isEmpty) return []; + List listsSearchResults = await fts_helpers.search(query, 'lists'); + List todoItemsSearchResults = await fts_helpers.search(query, 'todos'); + List formattedListResults = listsSearchResults + .map((result) => {"id": result['id'], "name": result['name']}) + .toList(); + List formattedTodoItemsResults = todoItemsSearchResults + .map((result) => { + // Use list_id so the navigation goes to the list page + "id": result['list_id'], + "name": result['description'], + }) + .toList(); + List formattedResults = [ + ...formattedListResults, + ...formattedTodoItemsResults + ]; + return formattedResults; + } +} diff --git a/demos/supabase-todolist-new-attachment/lib/widgets/guard_by_sync.dart b/demos/supabase-todolist-new-attachment/lib/widgets/guard_by_sync.dart new file mode 100644 index 00000000..dbd233e0 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/lib/widgets/guard_by_sync.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:powersync/powersync.dart' hide Column; +import 'package:powersync_flutter_demo_new/powersync.dart'; + +/// A widget that shows [child] after a complete sync on the database has +/// completed and a progress bar before that. +class GuardBySync extends StatelessWidget { + final Widget child; + + /// When set, wait only for a complete sync within the [BucketPriority] + /// instead of a full sync. + final BucketPriority? priority; + + const GuardBySync({ + super.key, + required this.child, + this.priority, + }); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: db.statusStream, + initialData: db.currentStatus, + builder: (context, snapshot) { + final status = snapshot.requireData; + final (didSync, progress) = switch (priority) { + null => (status.hasSynced ?? false, status.downloadProgress), + var priority? => ( + status.statusForPriority(priority).hasSynced ?? false, + status.downloadProgress?.untilPriority(priority) + ), + }; + + if (didSync) { + return child; + } else { + return Center( + child: Column( + children: [ + const Text('Busy with sync...'), + LinearProgressIndicator(value: progress?.downloadedFraction), + if (progress case final progress?) + Text( + '${progress.downloadedOperations} out of ${progress.totalOperations}') + ], + ), + ); + } + }, + ); + } +} diff --git a/demos/supabase-todolist-new-attachment/lib/widgets/list_item.dart b/demos/supabase-todolist-new-attachment/lib/widgets/list_item.dart new file mode 100644 index 00000000..981b382a --- /dev/null +++ b/demos/supabase-todolist-new-attachment/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/supabase-todolist-new-attachment/lib/widgets/list_item_dialog.dart b/demos/supabase-todolist-new-attachment/lib/widgets/list_item_dialog.dart new file mode 100644 index 00000000..3fb8c133 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/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/supabase-todolist-new-attachment/lib/widgets/lists_page.dart b/demos/supabase-todolist-new-attachment/lib/widgets/lists_page.dart new file mode 100644 index 00000000..c41aabbe --- /dev/null +++ b/demos/supabase-todolist-new-attachment/lib/widgets/lists_page.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:powersync/powersync.dart'; + +import './list_item.dart'; +import './list_item_dialog.dart'; +import '../main.dart'; +import '../models/todo_list.dart'; +import 'guard_by_sync.dart'; + +void _showAddDialog(BuildContext context) async { + return showDialog( + context: context, + barrierDismissible: false, // user must tap button! + builder: (BuildContext context) { + return const ListItemDialog(); + }, + ); +} + +class ListsPage extends StatelessWidget { + const ListsPage({super.key}); + + @override + Widget build(BuildContext context) { + const content = ListsWidget(); + + final button = FloatingActionButton( + onPressed: () { + _showAddDialog(context); + }, + tooltip: 'Create List', + child: const Icon(Icons.add), + ); + + final page = MyHomePage( + title: 'Todo Lists', + content: content, + floatingActionButton: button, + ); + return page; + } +} + +class ListsWidget extends StatelessWidget { + const ListsWidget({super.key}); + + @override + Widget build(BuildContext context) { + return GuardBySync( + priority: _listsPriority, + child: StreamBuilder( + stream: TodoList.watchListsWithStats(), + builder: (context, snapshot) { + if (snapshot.data case final todoLists?) { + return ListView( + padding: const EdgeInsets.symmetric(vertical: 8.0), + children: todoLists.map((list) { + return ListItemWidget(list: list); + }).toList(), + ); + } else { + return const CircularProgressIndicator(); + } + }, + ), + ); + } + + static final _listsPriority = BucketPriority(1); +} diff --git a/demos/supabase-todolist-new-attachment/lib/widgets/login_page.dart b/demos/supabase-todolist-new-attachment/lib/widgets/login_page.dart new file mode 100644 index 00000000..f54f09da --- /dev/null +++ b/demos/supabase-todolist-new-attachment/lib/widgets/login_page.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; +import 'package:supabase_flutter/supabase_flutter.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 { + setState(() { + _busy = true; + _error = null; + }); + try { + await Supabase.instance.client.auth.signInWithPassword( + email: _usernameController.text, password: _passwordController.text); + + if (context.mounted) { + Navigator.of(context).pushReplacement(MaterialPageRoute( + builder: (context) => listsPage, + )); + } + } on AuthException catch (e) { + setState(() { + _error = e.message; + }); + } catch (e) { + setState(() { + _error = e.toString(); + }); + } finally { + setState(() { + _busy = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("PowerSync Flutter 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('Supabase 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'), + ), + TextButton( + onPressed: _busy + ? null + : () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => signupPage, + )); + }, + child: const Text('Sign Up'), + ), + ], + ), + ), + ), + ), + ), + )); + } +} diff --git a/demos/supabase-todolist-new-attachment/lib/widgets/query_widget.dart b/demos/supabase-todolist-new-attachment/lib/widgets/query_widget.dart new file mode 100644 index 00000000..426c7060 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/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/supabase-todolist-new-attachment/lib/widgets/resultset_table.dart b/demos/supabase-todolist-new-attachment/lib/widgets/resultset_table.dart new file mode 100644 index 00000000..f348e4ff --- /dev/null +++ b/demos/supabase-todolist-new-attachment/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/supabase-todolist-new-attachment/lib/widgets/signup_page.dart b/demos/supabase-todolist-new-attachment/lib/widgets/signup_page.dart new file mode 100644 index 00000000..2f9150b4 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/lib/widgets/signup_page.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +import '../main.dart'; + +class SignupPage extends StatefulWidget { + const SignupPage({super.key}); + + @override + State createState() => _SignupPageState(); +} + +class _SignupPageState 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 _signup(BuildContext context) async { + setState(() { + _busy = true; + _error = null; + }); + try { + final response = await Supabase.instance.client.auth.signUp( + email: _usernameController.text, password: _passwordController.text); + + if (context.mounted) { + if (response.session != null) { + Navigator.of(context).pushReplacement(MaterialPageRoute( + builder: (context) => homePage, + )); + } else { + Navigator.of(context).pop(); + } + } + } on AuthException catch (e) { + setState(() { + _error = e.message; + }); + } catch (e) { + setState(() { + _error = e.toString(); + }); + } finally { + setState(() { + _busy = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("PowerSync Flutter 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('Sign Up'), + const SizedBox(height: 35), + TextFormField( + controller: _usernameController, + decoration: const InputDecoration(labelText: "Email"), + enabled: !_busy, + onFieldSubmitted: _busy + ? null + : (String value) { + _signup(context); + }, + ), + const SizedBox(height: 20), + TextFormField( + obscureText: true, + controller: _passwordController, + decoration: InputDecoration( + labelText: "Password", errorText: _error), + enabled: !_busy, + onFieldSubmitted: _busy + ? null + : (String value) { + _signup(context); + }, + ), + const SizedBox(height: 25), + TextButton( + onPressed: _busy + ? null + : () { + _signup(context); + }, + child: const Text('Sign Up'), + ), + ], + ), + ), + ), + ), + ), + )); + } +} diff --git a/demos/supabase-todolist-new-attachment/lib/widgets/status_app_bar.dart b/demos/supabase-todolist-new-attachment/lib/widgets/status_app_bar.dart new file mode 100644 index 00000000..a520aa19 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/lib/widgets/status_app_bar.dart @@ -0,0 +1,74 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:powersync/powersync.dart'; +import 'package:powersync_flutter_demo_new/widgets/fts_search_delegate.dart'; +import '../powersync.dart'; + +class StatusAppBar extends StatelessWidget implements PreferredSizeWidget { + final Widget title; + + const StatusAppBar({super.key, required this.title}); + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: db.statusStream, + initialData: db.currentStatus, + builder: (context, snapshot) { + final status = snapshot.data!; + final statusIcon = _getStatusIcon(status); + + return AppBar( + title: title, + actions: [ + 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/supabase-todolist-new-attachment/lib/widgets/todo_item_dialog.dart b/demos/supabase-todolist-new-attachment/lib/widgets/todo_item_dialog.dart new file mode 100644 index 00000000..641abd7f --- /dev/null +++ b/demos/supabase-todolist-new-attachment/lib/widgets/todo_item_dialog.dart @@ -0,0 +1,65 @@ +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 initState() { + super.initState(); + } + + @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/supabase-todolist-new-attachment/lib/widgets/todo_item_widget.dart b/demos/supabase-todolist-new-attachment/lib/widgets/todo_item_widget.dart new file mode 100644 index 00000000..66962790 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/lib/widgets/todo_item_widget.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:powersync_flutter_demo_new/app_config.dart'; +import 'package:powersync_flutter_demo_new/attachments/photo_widget.dart'; +import 'package:powersync_flutter_demo_new/attachments/queue.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 { + if (todo.photoId != null) { + print('deleteTodo: ${todo.photoId}'); + + await attachmentQueue.deleteFile( + attachmentId: todo.photoId!, + updateHook: (context, attachment) async { + // await context.execute("UPDATE todos SET photo_id = NULL WHERE id = ?", [todo.id]); + }, + ); + } + await todo.delete(); + } + + @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', + ), + AppConfig.supabaseStorageBucket.isEmpty + ? Container() + : PhotoWidget(todo: todo), + ], + )); + } +} diff --git a/demos/supabase-todolist-new-attachment/lib/widgets/todo_list_page.dart b/demos/supabase-todolist-new-attachment/lib/widgets/todo_list_page.dart new file mode 100644 index 00000000..7e28238e --- /dev/null +++ b/demos/supabase-todolist-new-attachment/lib/widgets/todo_list_page.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + +import '../powersync.dart'; +import './status_app_bar.dart'; +import './todo_item_dialog.dart'; +import './todo_item_widget.dart'; +import '../models/todo_list.dart'; + +void _showAddDialog(BuildContext context, TodoList list) async { + return showDialog( + context: context, + barrierDismissible: false, // user must tap button! + builder: (BuildContext context) { + return TodoItemDialog(list: list); + }, + ); +} + +class TodoListPage extends StatelessWidget { + final TodoList list; + + const TodoListPage({super.key, required this.list}); + + @override + Widget build(BuildContext context) { + final button = FloatingActionButton( + onPressed: () { + _showAddDialog(context, list); + }, + tooltip: 'Add Item', + child: const Icon(Icons.add), + ); + + return Scaffold( + appBar: StatusAppBar(title: Text(list.name)), + floatingActionButton: button, + body: TodoListWidget(list: list)); + } +} + +class TodoListWidget extends StatelessWidget { + final TodoList list; + + const TodoListWidget({super.key, required this.list}); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: TodoList.watchSyncStatus().map((e) => e.hasSynced), + initialData: db.currentStatus.hasSynced, + builder: (context, snapshot) { + return StreamBuilder( + stream: list.watchItems(), + builder: (context, snapshot) { + final items = snapshot.data ?? const []; + + return ListView( + padding: const EdgeInsets.symmetric(vertical: 8.0), + children: items.map((todo) { + return TodoItemWidget(todo: todo); + }).toList(), + ); + }, + ); + }, + ); + } +} diff --git a/demos/supabase-todolist-new-attachment/linux/.gitignore b/demos/supabase-todolist-new-attachment/linux/.gitignore new file mode 100644 index 00000000..d3896c98 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/demos/supabase-todolist-new-attachment/linux/CMakeLists.txt b/demos/supabase-todolist-new-attachment/linux/CMakeLists.txt new file mode 100644 index 00000000..df0a3887 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/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/supabase-todolist-new-attachment/linux/flutter/CMakeLists.txt b/demos/supabase-todolist-new-attachment/linux/flutter/CMakeLists.txt new file mode 100644 index 00000000..d5bd0164 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/demos/supabase-todolist-new-attachment/linux/flutter/generated_plugin_registrant.cc b/demos/supabase-todolist-new-attachment/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..1bef6a30 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,27 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); + g_autoptr(FlPluginRegistrar) powersync_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "PowersyncFlutterLibsPlugin"); + powersync_flutter_libs_plugin_register_with_registrar(powersync_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); + sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/demos/supabase-todolist-new-attachment/linux/flutter/generated_plugin_registrant.h b/demos/supabase-todolist-new-attachment/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..e0f0a47b --- /dev/null +++ b/demos/supabase-todolist-new-attachment/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/demos/supabase-todolist-new-attachment/linux/flutter/generated_plugins.cmake b/demos/supabase-todolist-new-attachment/linux/flutter/generated_plugins.cmake new file mode 100644 index 00000000..ed77a1a0 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/linux/flutter/generated_plugins.cmake @@ -0,0 +1,27 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + gtk + powersync_flutter_libs + sqlite3_flutter_libs + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/demos/supabase-todolist-new-attachment/linux/main.cc b/demos/supabase-todolist-new-attachment/linux/main.cc new file mode 100644 index 00000000..e7c5c543 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/demos/supabase-todolist-new-attachment/linux/my_application.cc b/demos/supabase-todolist-new-attachment/linux/my_application.cc new file mode 100644 index 00000000..7dcb7e37 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/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/supabase-todolist-new-attachment/linux/my_application.h b/demos/supabase-todolist-new-attachment/linux/my_application.h new file mode 100644 index 00000000..72271d5e --- /dev/null +++ b/demos/supabase-todolist-new-attachment/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/demos/supabase-todolist-new-attachment/linux/runner/CMakeLists.txt b/demos/supabase-todolist-new-attachment/linux/runner/CMakeLists.txt new file mode 100644 index 00000000..e97dabc7 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/demos/supabase-todolist-new-attachment/linux/runner/main.cc b/demos/supabase-todolist-new-attachment/linux/runner/main.cc new file mode 100644 index 00000000..e7c5c543 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/demos/supabase-todolist-new-attachment/linux/runner/my_application.cc b/demos/supabase-todolist-new-attachment/linux/runner/my_application.cc new file mode 100644 index 00000000..dde6b0ac --- /dev/null +++ b/demos/supabase-todolist-new-attachment/linux/runner/my_application.cc @@ -0,0 +1,130 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "powersync_flutter_demo"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "powersync_flutter_demo"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/demos/supabase-todolist-new-attachment/linux/runner/my_application.h b/demos/supabase-todolist-new-attachment/linux/runner/my_application.h new file mode 100644 index 00000000..72271d5e --- /dev/null +++ b/demos/supabase-todolist-new-attachment/linux/runner/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/demos/supabase-todolist-new-attachment/macos/.gitignore b/demos/supabase-todolist-new-attachment/macos/.gitignore new file mode 100644 index 00000000..746adbb6 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/demos/supabase-todolist-new-attachment/macos/Flutter/Flutter-Debug.xcconfig b/demos/supabase-todolist-new-attachment/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 00000000..4b81f9b2 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/demos/supabase-todolist-new-attachment/macos/Flutter/Flutter-Release.xcconfig b/demos/supabase-todolist-new-attachment/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 00000000..5caa9d15 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/demos/supabase-todolist-new-attachment/macos/Flutter/GeneratedPluginRegistrant.swift b/demos/supabase-todolist-new-attachment/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 00000000..0c6fd7ff --- /dev/null +++ b/demos/supabase-todolist-new-attachment/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,22 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +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 + +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-new-attachment/macos/Podfile b/demos/supabase-todolist-new-attachment/macos/Podfile new file mode 100644 index 00000000..c795730d --- /dev/null +++ b/demos/supabase-todolist-new-attachment/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/supabase-todolist-new-attachment/macos/Podfile.lock b/demos/supabase-todolist-new-attachment/macos/Podfile.lock new file mode 100644 index 00000000..f32a7411 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/macos/Podfile.lock @@ -0,0 +1,83 @@ +PODS: + - app_links (1.0.0): + - FlutterMacOS + - FlutterMacOS (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - powersync-sqlite-core (0.4.2) + - powersync_flutter_libs (0.0.1): + - FlutterMacOS + - powersync-sqlite-core (~> 0.4.2) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (3.49.2): + - sqlite3/common (= 3.49.2) + - sqlite3/common (3.49.2) + - sqlite3/dbstatvtab (3.49.2): + - sqlite3/common + - sqlite3/fts5 (3.49.2): + - sqlite3/common + - sqlite3/math (3.49.2): + - sqlite3/common + - sqlite3/perf-threadsafe (3.49.2): + - sqlite3/common + - sqlite3/rtree (3.49.2): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (~> 3.49.1) + - sqlite3/dbstatvtab + - sqlite3/fts5 + - sqlite3/math + - sqlite3/perf-threadsafe + - sqlite3/rtree + - url_launcher_macos (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - powersync_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/powersync_flutter_libs/macos`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + +SPEC REPOS: + trunk: + - powersync-sqlite-core + - sqlite3 + +EXTERNAL SOURCES: + app_links: + :path: Flutter/ephemeral/.symlinks/plugins/app_links/macos + FlutterMacOS: + :path: Flutter/ephemeral + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + powersync_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/powersync_flutter_libs/macos + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + sqlite3_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + +SPEC CHECKSUMS: + app_links: afe860c55c7ef176cea7fb630a2b7d7736de591d + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + powersync-sqlite-core: a58efd88833861f0a8bb636c171bdf0ed55c9801 + powersync_flutter_libs: fa885a30ceb636655741eee2ff5282d0500fa96b + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 + sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 + +PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 + +COCOAPODS: 1.16.2 diff --git a/demos/supabase-todolist-new-attachment/macos/Runner.xcodeproj/project.pbxproj b/demos/supabase-todolist-new-attachment/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..5581abf6 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/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 */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 75E48BA0AEB945CF7281B8D7 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 610323F3498FEA93EE8D7ECD /* Pods_Runner.framework */; }; + 8B5261612A7C463D00E9899E /* powersync_flutter_demoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B5261602A7C463D00E9899E /* powersync_flutter_demoTests.swift */; }; + 9381A48772266D9C49309994 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 991B2149AEB0B3DAB09CB3BE /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 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 */ + 10C22D07423926C20887DE48 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* powersync_flutter_demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = powersync_flutter_demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; + 4D425E3DE8C8153AB8C55A47 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 610323F3498FEA93EE8D7ECD /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 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 = ""; }; + 991B2149AEB0B3DAB09CB3BE /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AE0A38BE3137E8F7E92FEF51 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + D7EC9DA661EA44265DC94A0B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + DD914308A8C7B352FD10170F /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + EE686A5A317D10AC330E1BF9 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 75E48BA0AEB945CF7281B8D7 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8B52615B2A7C463D00E9899E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9381A48772266D9C49309994 /* 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 */, + 85152821BF893F70AB178223 /* Pods */, + B7D43289954627B8B1B9B1F8 /* Frameworks */, + ); + 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 = ""; + }; + 85152821BF893F70AB178223 /* Pods */ = { + isa = PBXGroup; + children = ( + 4D425E3DE8C8153AB8C55A47 /* Pods-Runner.debug.xcconfig */, + D7EC9DA661EA44265DC94A0B /* Pods-Runner.release.xcconfig */, + EE686A5A317D10AC330E1BF9 /* Pods-Runner.profile.xcconfig */, + AE0A38BE3137E8F7E92FEF51 /* Pods-RunnerTests.debug.xcconfig */, + 10C22D07423926C20887DE48 /* Pods-RunnerTests.release.xcconfig */, + DD914308A8C7B352FD10170F /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 8B52615F2A7C463D00E9899E /* powersync_flutter_demoTests */ = { + isa = PBXGroup; + children = ( + 8B5261602A7C463D00E9899E /* powersync_flutter_demoTests.swift */, + ); + path = powersync_flutter_demoTests; + sourceTree = ""; + }; + B7D43289954627B8B1B9B1F8 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 610323F3498FEA93EE8D7ECD /* Pods_Runner.framework */, + 991B2149AEB0B3DAB09CB3BE /* 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 = ( + 179983970B20315AFC123D9D /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + F30CD4FEA1BDB49C8B84FB01 /* [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 = ( + 6C234BEFE02FE63D2DBBAA50 /* [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 */ + 179983970B20315AFC123D9D /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + 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"; + }; + 6C234BEFE02FE63D2DBBAA50 /* [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; + }; + F30CD4FEA1BDB49C8B84FB01 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 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 = AE0A38BE3137E8F7E92FEF51 /* 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 = 10C22D07423926C20887DE48 /* 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 = DD914308A8C7B352FD10170F /* 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/supabase-todolist-new-attachment/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demos/supabase-todolist-new-attachment/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/demos/supabase-todolist-new-attachment/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demos/supabase-todolist-new-attachment/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..943aed19 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/supabase-todolist-new-attachment/macos/Runner.xcworkspace/contents.xcworkspacedata b/demos/supabase-todolist-new-attachment/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/demos/supabase-todolist-new-attachment/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demos/supabase-todolist-new-attachment/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/AppDelegate.swift b/demos/supabase-todolist-new-attachment/macos/Runner/AppDelegate.swift new file mode 100644 index 00000000..b3c17614 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..a2ec33f1 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..82b6f9d9a33e198f5747104729e1fcef999772a5 GIT binary patch literal 102994 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? 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 z 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 Pe7HEdAEg|=c=LY&YVNkY literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..13b35eba55c6dabc3aac36f33d859266c18fa0d0 GIT binary patch literal 5680 zcmaiYXH?Tqu=Xz`p-L#B_gI#0we$cm_HcmYFP$?wjD#BaCN4mzC5#`>w9y6=ThxrYZc0WPXprg zYjB`UsV}0=eUtY$(P6YW}npdd;%9pi?zS3k-nqCob zSX_AQEf|=wYT3r?f!*Yt)ar^;l3Sro{z(7deUBPd2~(SzZ-s@0r&~Km2S?8r##9-< z)2UOSVaHqq6}%sA9Ww;V2LG=PnNAh6mA2iWOuV7T_lRDR z&N8-eN=U)-T|;wo^Wv=34wtV0g}sAAe}`Ph@~!|<;z7*K8(qkX0}o=!(+N*UWrkEja*$_H6mhK1u{P!AC39} z|3+Z(mAOq#XRYS)TLoHv<)d%$$I@+x+2)V{@o~~J-!YUI-Q9%!Ldi4Op&Lw&B>jj* zwAgC#Y>gbIqv!d|J5f!$dbCXoq(l3GR(S>(rtZ~Z*agXMMKN!@mWT_vmCbSd3dUUm z4M&+gz?@^#RRGal%G3dDvj7C5QTb@9+!MG+>0dcjtZEB45c+qx*c?)d<%htn1o!#1 zpIGonh>P1LHu3s)fGFF-qS}AXjW|M*2Xjkh7(~r(lN=o#mBD9?jt74=Rz85I4Nfx_ z7Z)q?!};>IUjMNM6ee2Thq7))a>My?iWFxQ&}WvsFP5LP+iGz+QiYek+K1`bZiTV- zHHYng?ct@Uw5!gquJ(tEv1wTrRR7cemI>aSzLI^$PxW`wL_zt@RSfZ1M3c2sbebM* ze0=;sy^!90gL~YKISz*x;*^~hcCoO&CRD)zjT(A2b_uRue=QXFe5|!cf0z1m!iwv5GUnLw9Dr*Ux z)3Lc!J@Ei;&&yxGpf2kn@2wJ2?t6~obUg;?tBiD#uo$SkFIasu+^~h33W~`r82rSa ztyE;ehFjC2hjpJ-e__EH&z?!~>UBb=&%DS>NT)1O3Isn-!SElBV2!~m6v0$vx^a<@ISutdTk1@?;i z<8w#b-%|a#?e5(n@7>M|v<<0Kpg?BiHYMRe!3Z{wYc2hN{2`6(;q`9BtXIhVq6t~KMH~J0~XtUuT06hL8c1BYZWhN zk4F2I;|za*R{ToHH2L?MfRAm5(i1Ijw;f+0&J}pZ=A0;A4M`|10ZskA!a4VibFKn^ zdVH4OlsFV{R}vFlD~aA4xxSCTTMW@Gws4bFWI@xume%smAnuJ0b91QIF?ZV!%VSRJ zO7FmG!swKO{xuH{DYZ^##gGrXsUwYfD0dxXX3>QmD&`mSi;k)YvEQX?UyfIjQeIm! z0ME3gmQ`qRZ;{qYOWt}$-mW*>D~SPZKOgP)T-Sg%d;cw^#$>3A9I(%#vsTRQe%moT zU`geRJ16l>FV^HKX1GG7fR9AT((jaVb~E|0(c-WYQscVl(z?W!rJp`etF$dBXP|EG z=WXbcZ8mI)WBN>3<@%4eD597FD5nlZajwh8(c$lum>yP)F}=(D5g1-WVZRc)(!E3} z-6jy(x$OZOwE=~{EQS(Tp`yV2&t;KBpG*XWX!yG+>tc4aoxbXi7u@O*8WWFOxUjcq z^uV_|*818$+@_{|d~VOP{NcNi+FpJ9)aA2So<7sB%j`$Prje&auIiTBb{oD7q~3g0 z>QNIwcz(V-y{Ona?L&=JaV5`o71nIsWUMA~HOdCs10H+Irew#Kr(2cn>orG2J!jvP zqcVX0OiF}c<)+5&p}a>_Uuv)L_j}nqnJ5a?RPBNi8k$R~zpZ33AA4=xJ@Z($s3pG9 zkURJY5ZI=cZGRt_;`hs$kE@B0FrRx(6K{`i1^*TY;Vn?|IAv9|NrN*KnJqO|8$e1& zb?OgMV&q5|w7PNlHLHF) zB+AK#?EtCgCvwvZ6*u|TDhJcCO+%I^@Td8CR}+nz;OZ*4Dn?mSi97m*CXXc=};!P`B?}X`F-B5v-%ACa8fo0W++j&ztmqK z;&A)cT4ob9&MxpQU41agyMU8jFq~RzXOAsy>}hBQdFVL%aTn~M>5t9go2j$i9=(rZ zADmVj;Qntcr3NIPPTggpUxL_z#5~C!Gk2Rk^3jSiDqsbpOXf^f&|h^jT4|l2ehPat zb$<*B+x^qO8Po2+DAmrQ$Zqc`1%?gp*mDk>ERf6I|42^tjR6>}4`F_Mo^N(~Spjcg z_uY$}zui*PuDJjrpP0Pd+x^5ds3TG#f?57dFL{auS_W8|G*o}gcnsKYjS6*t8VI<) zcjqTzW(Hk*t-Qhq`Xe+x%}sxXRerScbPGv8hlJ;CnU-!Nl=# zR=iTFf9`EItr9iAlAGi}i&~nJ-&+)Y| zMZigh{LXe)uR+4D_Yb+1?I93mHQ5{pId2Fq%DBr7`?ipi;CT!Q&|EO3gH~7g?8>~l zT@%*5BbetH)~%TrAF1!-!=)`FIS{^EVA4WlXYtEy^|@y@yr!C~gX+cp2;|O4x1_Ol z4fPOE^nj(}KPQasY#U{m)}TZt1C5O}vz`A|1J!-D)bR%^+=J-yJsQXDzFiqb+PT0! zIaDWWU(AfOKlSBMS};3xBN*1F2j1-_=%o($ETm8@oR_NvtMDVIv_k zlnNBiHU&h8425{MCa=`vb2YP5KM7**!{1O>5Khzu+5OVGY;V=Vl+24fOE;tMfujoF z0M``}MNnTg3f%Uy6hZi$#g%PUA_-W>uVCYpE*1j>U8cYP6m(>KAVCmbsDf39Lqv0^ zt}V6FWjOU@AbruB7MH2XqtnwiXS2scgjVMH&aF~AIduh#^aT1>*V>-st8%=Kk*{bL zzbQcK(l2~)*A8gvfX=RPsNnjfkRZ@3DZ*ff5rmx{@iYJV+a@&++}ZW+za2fU>&(4y`6wgMpQGG5Ah(9oGcJ^P(H< zvYn5JE$2B`Z7F6ihy>_49!6}(-)oZ(zryIXt=*a$bpIw^k?>RJ2 zQYr>-D#T`2ZWDU$pM89Cl+C<;J!EzHwn(NNnWpYFqDDZ_*FZ{9KQRcSrl5T>dj+eA zi|okW;6)6LR5zebZJtZ%6Gx8^=2d9>_670!8Qm$wd+?zc4RAfV!ZZ$jV0qrv(D`db zm_T*KGCh3CJGb(*X6nXzh!h9@BZ-NO8py|wG8Qv^N*g?kouH4%QkPU~Vizh-D3<@% zGomx%q42B7B}?MVdv1DFb!axQ73AUxqr!yTyFlp%Z1IAgG49usqaEbI_RnbweR;Xs zpJq7GKL_iqi8Md?f>cR?^0CA+Uk(#mTlGdZbuC*$PrdB$+EGiW**=$A3X&^lM^K2s zzwc3LtEs5|ho z2>U(-GL`}eNgL-nv3h7E<*<>C%O^=mmmX0`jQb6$mP7jUKaY4je&dCG{x$`0=_s$+ zSpgn!8f~ya&U@c%{HyrmiW2&Wzc#Sw@+14sCpTWReYpF9EQ|7vF*g|sqG3hx67g}9 zwUj5QP2Q-(KxovRtL|-62_QsHLD4Mu&qS|iDp%!rs(~ah8FcrGb?Uv^Qub5ZT_kn%I^U2rxo1DDpmN@8uejxik`DK2~IDi1d?%~pR7i#KTS zA78XRx<(RYO0_uKnw~vBKi9zX8VnjZEi?vD?YAw}y+)wIjIVg&5(=%rjx3xQ_vGCy z*&$A+bT#9%ZjI;0w(k$|*x{I1c!ECMus|TEA#QE%#&LxfGvijl7Ih!B2 z6((F_gwkV;+oSKrtr&pX&fKo3s3`TG@ye+k3Ov)<#J|p8?vKh@<$YE@YIU1~@7{f+ zydTna#zv?)6&s=1gqH<-piG>E6XW8ZI7&b@-+Yk0Oan_CW!~Q2R{QvMm8_W1IV8<+ zQTyy=(Wf*qcQubRK)$B;QF}Y>V6d_NM#=-ydM?%EPo$Q+jkf}*UrzR?Nsf?~pzIj$ z<$wN;7c!WDZ(G_7N@YgZ``l;_eAd3+;omNjlpfn;0(B7L)^;;1SsI6Le+c^ULe;O@ zl+Z@OOAr4$a;=I~R0w4jO`*PKBp?3K+uJ+Tu8^%i<_~bU!p%so z^sjol^slR`W@jiqn!M~eClIIl+`A5%lGT{z^mRbpv}~AyO%R*jmG_Wrng{B9TwIuS z0!@fsM~!57K1l0%{yy(#no}roy#r!?0wm~HT!vLDfEBs9x#`9yCKgufm0MjVRfZ=f z4*ZRc2Lgr(P+j2zQE_JzYmP0*;trl7{*N341Cq}%^M^VC3gKG-hY zmPT>ECyrhIoFhnMB^qpdbiuI}pk{qPbK^}0?Rf7^{98+95zNq6!RuV_zAe&nDk0;f zez~oXlE5%ve^TmBEt*x_X#fs(-En$jXr-R4sb$b~`nS=iOy|OVrph(U&cVS!IhmZ~ zKIRA9X%Wp1J=vTvHZ~SDe_JXOe9*fa zgEPf;gD^|qE=dl>Qkx3(80#SE7oxXQ(n4qQ#by{uppSKoDbaq`U+fRqk0BwI>IXV3 zD#K%ASkzd7u>@|pA=)Z>rQr@dLH}*r7r0ng zxa^eME+l*s7{5TNu!+bD{Pp@2)v%g6^>yj{XP&mShhg9GszNu4ITW=XCIUp2Xro&1 zg_D=J3r)6hp$8+94?D$Yn2@Kp-3LDsci)<-H!wCeQt$e9Jk)K86hvV^*Nj-Ea*o;G zsuhRw$H{$o>8qByz1V!(yV{p_0X?Kmy%g#1oSmlHsw;FQ%j9S#}ha zm0Nx09@jmOtP8Q+onN^BAgd8QI^(y!n;-APUpo5WVdmp8!`yKTlF>cqn>ag`4;o>i zl!M0G-(S*fm6VjYy}J}0nX7nJ$h`|b&KuW4d&W5IhbR;-)*9Y0(Jj|@j`$xoPQ=Cl literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..0a3f5fa40fb3d1e0710331a48de5d256da3f275d GIT binary patch literal 520 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uuz(rC1}QWNE&K#jR^;j87-Auq zoUlN^K{r-Q+XN;zI ze|?*NFmgt#V#GwrSWaz^2G&@SBmck6ZcIFMww~vE<1E?M2#KUn1CzsB6D2+0SuRV@ zV2kK5HvIGB{HX-hQzs0*AB%5$9RJ@a;)Ahq#p$GSP91^&hi#6sg*;a~dt}4AclK>h z_3MoPRQ{i;==;*1S-mY<(JFzhAxMI&<61&m$J0NDHdJ3tYx~j0%M-uN6Zl8~_0DOkGXc0001@sz3l12C6Xg{AT~( zm6w64BA|AX`Ve)YY-glyudNN>MAfkXz-T7`_`fEolM;0T0BA)(02-OaW z0*cW7Z~ec94o8&g0D$N>b!COu{=m}^%oXZ4?T8ZyPZuGGBPBA7pbQMoV5HYhiT?%! zcae~`(QAN4&}-=#2f5fkn!SWGWmSeCISBcS=1-U|MEoKq=k?_x3apK>9((R zuu$9X?^8?@(a{qMS%J8SJPq))v}Q-ZyDm6Gbie0m92=`YlwnQPQP1kGSm(N2UJ3P6 z^{p-u)SSCTW~c1rw;cM)-uL2{->wCn2{#%;AtCQ!m%AakVs1K#v@(*-6QavyY&v&*wO_rCJXJuq$c$7ZjsW+pJo-$L^@!7X04CvaOpPyfw|FKvu;e(&Iw>Tbg zL}#8e^?X%TReXTt>gsBByt0kSU20oQx*~P=4`&tcZ7N6t-6LiK{LxX*p6}9c<0Pu^ zLx1w_P4P2V>bX=`F%v$#{sUDdF|;rbI{p#ZW`00Bgh(eB(nOIhy8W9T>3aQ=k8Z9% zB+TusFABF~J?N~fAd}1Rme=@4+1=M{^P`~se7}e3;mY0!%#MJf!XSrUC{0uZqMAd7%q zQY#$A>q}noIB4g54Ue)x>ofVm3DKBbUmS4Z-bm7KdKsUixva)1*&z5rgAG2gxG+_x zqT-KNY4g7eM!?>==;uD9Y4iI(Hu$pl8!LrK_Zb}5nv(XKW{9R144E!cFf36p{i|8pRL~p`_^iNo z{mf7y`#hejw#^#7oKPlN_Td{psNpNnM?{7{R-ICBtYxk>?3}OTH_8WkfaTLw)ZRTfxjW+0>gMe zpKg~`Bc$Y>^VX;ks^J0oKhB#6Ukt{oQhN+o2FKGZx}~j`cQB%vVsMFnm~R_1Y&Ml? zwFfb~d|dW~UktY@?zkau>Owe zRroi(<)c4Ux&wJfY=3I=vg)uh;sL(IYY9r$WK1$F;jYqq1>xT{LCkIMb3t2jN8d`9 z=4(v-z7vHucc_fjkpS}mGC{ND+J-hc_0Ix4kT^~{-2n|;Jmn|Xf9wGudDk7bi*?^+ z7fku8z*mbkGm&xf&lmu#=b5mp{X(AwtLTf!N`7FmOmX=4xwbD=fEo8CaB1d1=$|)+ z+Dlf^GzGOdlqTO8EwO?8;r+b;gkaF^$;+#~2_YYVH!hD6r;PaWdm#V=BJ1gH9ZK_9 zrAiIC-)z)hRq6i5+$JVmR!m4P>3yJ%lH)O&wtCyum3A*})*fHODD2nq!1@M>t@Za+ zH6{(Vf>_7!I-APmpsGLYpl7jww@s5hHOj5LCQXh)YAp+y{gG(0UMm(Ur z3o3n36oFwCkn+H*GZ-c6$Y!5r3z*@z0`NrB2C^q#LkOuooUM8Oek2KBk}o1PU8&2L z4iNkb5CqJWs58aR394iCU^ImDqV;q_Pp?pl=RB2372(Io^GA^+oKguO1(x$0<7w3z z)j{vnqEB679Rz4i4t;8|&Zg77UrklxY9@GDq(ZphH6=sW`;@uIt5B?7Oi?A0-BL}(#1&R;>2aFdq+E{jsvpNHjLx2t{@g1}c~DQcPNmVmy| zNMO@ewD^+T!|!DCOf}s9dLJU}(KZy@Jc&2Nq3^;vHTs}Hgcp`cw&gd7#N}nAFe3cM1TF%vKbKSffd&~FG9y$gLyr{#to)nxz5cCASEzQ}gz8O)phtHuKOW6p z@EQF(R>j%~P63Wfosrz8p(F=D|Mff~chUGn(<=CQbSiZ{t!e zeDU-pPsLgtc#d`3PYr$i*AaT!zF#23htIG&?QfcUk+@k$LZI}v+js|yuGmE!PvAV3 ztzh90rK-0L6P}s?1QH`Ot@ilbgMBzWIs zIs6K<_NL$O4lwR%zH4oJ+}JJp-bL6~%k&p)NGDMNZX7)0kni&%^sH|T?A)`z z=adV?!qnWx^B$|LD3BaA(G=ePL1+}8iu^SnnD;VE1@VLHMVdSN9$d)R(Wk{JEOp(P zm3LtAL$b^*JsQ0W&eLaoYag~=fRRdI>#FaELCO7L>zXe6w*nxN$Iy*Q*ftHUX0+N- zU>{D_;RRVPbQ?U+$^%{lhOMKyE5>$?U1aEPist+r)b47_LehJGTu>TcgZe&J{ z{q&D{^Ps~z7|zj~rpoh2I_{gAYNoCIJmio3B}$!5vTF*h$Q*vFj~qbo%bJCCRy509 zHTdDh_HYH8Zb9`}D5;;J9fkWOQi%Y$B1!b9+ESj+B@dtAztlY2O3NE<6HFiqOF&p_ zW-K`KiY@RPSY-p9Q99}Hcd05DT79_pfb{BV7r~?9pWh=;mcKBLTen%THFPo2NN~Nf zriOtFnqx}rtO|A6k!r6 zf-z?y-UD{dT0kT9FJ`-oWuPHbo+3wBS(}?2ql(+e@VTExmfnB*liCb zmeI+v5*+W_L;&kQN^ChW{jE0Mw#0Tfs}`9bk3&7UjxP^Ke(%eJu2{VnW?tu7Iqecm zB5|=-QdzK$=h50~{X3*w4%o1FS_u(dG2s&427$lJ?6bkLet}yYXCy)u_Io1&g^c#( z-$yYmSpxz{>BL;~c+~sxJIe1$7eZI_9t`eB^Pr0)5CuA}w;;7#RvPq|H6!byRzIJG ziQ7a4y_vhj(AL`8PhIm9edCv|%TX#f50lt8+&V+D4<}IA@S@#f4xId80oH$!_!q?@ zFRGGg2mTv&@76P7aTI{)Hu%>3QS_d)pQ%g8BYi58K~m-Ov^7r8BhX7YC1D3vwz&N8{?H*_U7DI?CI)+et?q|eGu>42NJ?K4SY zD?kc>h@%4IqNYuQ8m10+8xr2HYg2qFNdJl=Tmp&ybF>1>pqVfa%SsV*BY$d6<@iJA ziyvKnZ(~F9xQNokBgMci#pnZ}Igh0@S~cYcU_2Jfuf|d3tuH?ZSSYBfM(Y3-JBsC|S9c;# zyIMkPxgrq};0T09pjj#X?W^TFCMf1-9P{)g88;NDI+S4DXe>7d3Mb~i-h&S|Jy{J< zq3736$bH?@{!amD!1Ys-X)9V=#Z={fzsjVYMX5BG6%}tkzwC#1nQLj1y1f#}8**4Y zAvDZHw8)N)8~oWC88CgzbwOrL9HFbk4}h85^ptuu7A+uc#$f^9`EWv1Vr{5+@~@Uv z#B<;-nt;)!k|fRIg;2DZ(A2M2aC65kOIov|?Mhi1Sl7YOU4c$T(DoRQIGY`ycfkn% zViHzL;E*A{`&L?GP06Foa38+QNGA zw3+Wqs(@q+H{XLJbwZzE(omw%9~LPZfYB|NF5%j%E5kr_xE0u;i?IOIchn~VjeDZ) zAqsqhP0vu2&Tbz3IgJvMpKbThC-@=nk)!|?MIPP>MggZg{cUcKsP8|N#cG5 zUXMXxcXBF9`p>09IR?x$Ry3;q@x*%}G#lnB1}r#!WL88I@uvm}X98cZ8KO&cqT1p> z+gT=IxPsq%n4GWgh-Bk8E4!~`r@t>DaQKsjDqYc&h$p~TCh8_Mck5UB84u6Jl@kUZCU9BA-S!*bf>ZotFX9?a_^y%)yH~rsAz0M5#^Di80_tgoKw(egN z`)#(MqAI&A84J#Z<|4`Co8`iY+Cv&iboMJ^f9ROUK0Lm$;-T*c;TCTED_0|qfhlcS zv;BD*$Zko#nWPL}2K8T-?4}p{u)4xon!v_(yVW8VMpxg4Kh^J6WM{IlD{s?%XRT8P|yCU`R&6gwB~ zg}{At!iWCzOH37!ytcPeC`(({ovP7M5Y@bYYMZ}P2Z3=Y_hT)4DRk}wfeIo%q*M9UvXYJq!-@Ly79m5aLD{hf@BzQB>FdQ4mw z6$@vzSKF^Gnzc9vbccii)==~9H#KW<6)Uy1wb~auBn6s`ct!ZEos`WK8e2%<00b%# zY9Nvnmj@V^K(a_38dw-S*;G-(i(ETuIwyirs?$FFW@|66a38k+a%GLmucL%Wc8qk3 z?h_4!?4Y-xt)ry)>J`SuY**fuq2>u+)VZ+_1Egzctb*xJ6+7q`K$^f~r|!i?(07CD zH!)C_uerf-AHNa?6Y61D_MjGu*|wcO+ZMOo4q2bWpvjEWK9yASk%)QhwZS%N2_F4& z16D18>e%Q1mZb`R;vW{+IUoKE`y3(7p zplg5cBB)dtf^SdLd4n60oWie|(ZjgZa6L*VKq02Aij+?Qfr#1z#fwh92aV-HGd^_w zsucG24j8b|pk>BO7k8dS86>f-jBP^Sa}SF{YNn=^NU9mLOdKcAstv&GV>r zLxKHPkFxpvE8^r@MSF6UA}cG`#yFL8;kA7ccH9D=BGBtW2;H>C`FjnF^P}(G{wU;G z!LXLCbPfsGeLCQ{Ep$^~)@?v`q(uI`CxBY44osPcq@(rR-633!qa zsyb>?v%@X+e|Mg`+kRL*(;X>^BNZz{_kw5+K;w?#pReiw7eU8_Z^hhJ&fj80XQkuU z39?-z)6Fy$I`bEiMheS(iB6uLmiMd1i)cbK*9iPpl+h4x9ch7x- z1h4H;W_G?|)i`z??KNJVwgfuAM=7&Apd3vm#AT8uzQZ!NII}}@!j)eIfn53h{NmN7 zAKG6SnKP%^k&R~m5#@_4B@V?hYyHkm>0SQ@PPiw*@Tp@UhP-?w@jW?nxXuCipMW=L zH*5l*d@+jXm0tIMP_ec6Jcy6$w(gKK@xBX8@%oPaSyG;13qkFb*LuVx3{AgIyy&n3 z@R2_DcEn|75_?-v5_o~%xEt~ONB>M~tpL!nOVBLPN&e5bn5>+7o0?Nm|EGJ5 zmUbF{u|Qn?cu5}n4@9}g(G1JxtzkKv(tqwm_?1`?YSVA2IS4WI+*(2D*wh&6MIEhw z+B+2U<&E&|YA=3>?^i6)@n1&&;WGHF-pqi_sN&^C9xoxME5UgorQ_hh1__zzR#zVC zOQt4q6>ME^iPJ37*(kg4^=EFqyKH@6HEHXy79oLj{vFqZGY?sVjk!BX^h$SFJlJnv z5uw~2jLpA)|0=tp>qG*tuLru?-u`khGG2)o{+iDx&nC}eWj3^zx|T`xn5SuR;Aw8U z`p&>dJw`F17@J8YAuW4=;leBE%qagVTG5SZdh&d)(#ZhowZ|cvWvGMMrfVsbg>_~! z19fRz8CSJdrD|Rl)w!uznBF&2-dg{>y4l+6(L(vzbLA0Bk&`=;oQQ>(M8G=3kto_) zP8HD*n4?MySO2YrG6fwSrVmnesW+D&fxjfEmp=tPd?RKLZJcH&K(-S+x)2~QZ$c(> zru?MND7_HPZJVF%wX(49H)+~!7*!I8w72v&{b={#l9yz+S_aVPc_So%iF8>$XD1q1 zFtucO=rBj0Ctmi0{njN8l@}!LX}@dwl>3yMxZ;7 z0Ff2oh8L)YuaAGOuZ5`-p%Z4H@H$;_XRJQ|&(MhO78E|nyFa158gAxG^SP(vGi^+< zChY}o(_=ci3Wta#|K6MVljNe0T$%Q5ylx-v`R)r8;3+VUpp-)7T`-Y&{Zk z*)1*2MW+_eOJtF5tCMDV`}jg-R(_IzeE9|MBKl;a7&(pCLz}5<Zf+)T7bgNUQ_!gZtMlw=8doE}#W+`Xp~1DlE=d5SPT?ymu!r4z%&#A-@x^=QfvDkfx5-jz+h zoZ1OK)2|}_+UI)i9%8sJ9X<7AA?g&_Wd7g#rttHZE;J*7!e5B^zdb%jBj&dUDg4&B zMMYrJ$Z%t!5z6=pMGuO-VF~2dwjoXY+kvR>`N7UYfIBMZGP|C7*O=tU z2Tg_xi#Q3S=1|=WRfZD;HT<1D?GMR%5kI^KWwGrC@P2@R>mDT^3qsmbBiJc21kip~ zZp<7;^w{R;JqZ)C4z-^wL=&dBYj9WJBh&rd^A^n@07qM$c+kGv^f+~mU5_*|eePF| z3wDo-qaoRjmIw<2DjMTG4$HP{z54_te_{W^gu8$r=q0JgowzgQPct2JNtWPUsjF8R zvit&V8$(;7a_m%%9TqPkCXYUp&k*MRcwr*24>hR! z$4c#E=PVE=P4MLTUBM z7#*RDe0}=B)(3cvNpOmWa*eH#2HR?NVqXdJ=hq);MGD07JIQQ7Y0#iD!$C+mk7x&B zMwkS@H%>|fmSu#+ zI!}Sb(%o29Vkp_Th>&&!k7O>Ba#Om~B_J{pT7BHHd8(Ede(l`7O#`_}19hr_?~JP9 z`q(`<)y>%)x;O7)#-wfCP{?llFMoH!)ZomgsOYFvZ1DxrlYhkWRw#E-#Qf*z@Y-EQ z1~?_=c@M4DO@8AzZ2hKvw8CgitzI9yFd&N1-{|vP#4IqYb*#S0e3hrjsEGlnc4xwk z4o!0rxpUt8j&`mJ8?+P8G{m^jbk)bo_UPM+ifW*y-A*et`#_Ja_3nYyRa9fAG1Xr5 z>#AM_@PY|*u)DGRWJihZvgEh#{*joJN28uN7;i5{kJ*Gb-TERfN{ERe_~$Es~NJCpdKLRvdj4658uYYx{ng7I<6j~w@p%F<7a(Ssib|j z51;=Py(Nu*#hnLx@w&8X%=jrADn3TW>kplnb zYbFIWWVQXN7%Cwn6KnR)kYePEBmvM45I)UJb$)ninpdYg3a5N6pm_7Q+9>!_^xy?k za8@tJ@OOs-pRAAfT>Nc2x=>sZUs2!9Dwa%TTmDggH4fq(x^MW>mcRyJINlAqK$YQCMgR8`>6=Sg$ zFnJZsA8xUBXIN3i70Q%8px@yQPMgVP=>xcPI38jNJK<=6hC={a07+n@R|$bnhB)X$ z(Zc%tadp70vBTnW{OUIjTMe38F}JIH$#A}PB&RosPyFZMD}q}5W%$rh>5#U;m`z2K zc(&WRxx7DQLM-+--^w*EWAIS%bi>h587qkwu|H=hma3T^bGD&Z!`u(RKLeNZ&pI=q$|HOcji(0P1QC!YkAp*u z3%S$kumxR}jU<@6`;*-9=5-&LYRA<~uFrwO3U0k*4|xUTp4ZY7;Zbjx|uw&BWU$zK(w55pWa~#=f$c zNDW0O68N!xCy>G}(CX=;8hJLxAKn@Aj(dbZxO8a$+L$jK8$N-h@4$i8)WqD_%Snh4 zR?{O%k}>lr>w$b$g=VP8mckcCrjnp>uQl5F_6dPM8FWRqs}h`DpfCv20uZhyY~tr8 zkAYW4#yM;*je)n=EAb(q@5BWD8b1_--m$Q-3wbh1hM{8ihq7UUQfg@)l06}y+#=$( z$x>oVYJ47zAC^>HLRE-!HitjUixP6!R98WU+h>zct7g4eD;Mj#FL*a!VW!v-@b(Jv zj@@xM5noCp5%Vk3vY{tyI#oyDV7<$`KG`tktVyC&0DqxA#>V;-3oH%NW|Q&=UQ&zU zXNIT67J4D%5R1k#bW0F}TD`hlW7b)-=-%X4;UxQ*u4bK$mTAp%y&-(?{sXF%e_VH6 zTkt(X)SSN|;8q@8XX6qfR;*$r#HbIrvOj*-5ND8RCrcw4u8D$LXm5zlj@E5<3S0R# z??=E$p{tOk96$SloZ~ARe5`J=dB|Nj?u|zy2r(-*(q^@YwZiTF@QzQyPx_l=IDKa) zqD@0?IHJqSqZ_5`)81?4^~`yiGh6>7?|dKa8!e|}5@&qV!Iu9<@G?E}Vx9EzomB3t zEbMEm$TKGwkHDpirp;FZD#6P5qIlQJ8}rf;lHoz#h4TFFPYmS3+8(13_Mx2`?^=8S z|0)0&dQLJTU6{b%*yrpQe#OKKCrL8}YKw+<#|m`SkgeoN69TzIBQOl_Yg)W*w?NW) z*WxhEp$zQBBazJSE6ygu@O^!@Fr46j=|K`Mmb~xbggw7<)BuC@cT@Bwb^k?o-A zKX^9AyqR?zBtW5UA#siILztgOp?r4qgC`9jYJG_fxlsVSugGprremg-W(K0{O!Nw-DN%=FYCyfYA3&p*K>+|Q}s4rx#CQK zNj^U;sLM#q8}#|PeC$p&jAjqMu(lkp-_50Y&n=qF9`a3`Pr9f;b`-~YZ+Bb0r~c+V z*JJ&|^T{}IHkwjNAaM^V*IQ;rk^hnnA@~?YL}7~^St}XfHf6OMMCd9!vhk#gRA*{L zp?&63axj|Si%^NW05#87zpU_>QpFNb+I00v@cHwvdBn+Un)n2Egdt~LcWOeBW4Okm zD$-e~RD+W|UB;KQ;a7GOU&%p*efGu2$@wR74+&iP8|6#_fmnh^WcJLs)rtz{46);F z4v0OL{ZP9550>2%FE(;SbM*#sqMl*UXOb>ch`fJ|(*bOZ9=EB1+V4fkQ)hjsm3-u^Pk-4ji_uDDHdD>84tER!MvbH`*tG zzvbhBR@}Yd`azQGavooV=<WbvWLlO#x`hyO34mKcxrGv=`{ssnP=0Be5#1B;Co9 zh{TR>tjW2Ny$ZxJpYeg57#0`GP#jxDCU0!H15nL@@G*HLQcRdcsUO3sO9xvtmUcc{F*>FQZcZ5bgwaS^k-j5mmt zI7Z{Xnoml|A(&_{imAjK!kf5>g(oDqDI4C{;Bv162k8sFNr;!qPa2LPh>=1n z=^_9)TsLDvTqK7&*Vfm5k;VXjBW^qN3Tl&}K=X5)oXJs$z3gk0_+7`mJvz{pK|FVs zHw!k&7xVjvY;|(Py<;J{)b#Yjj*LZO7x|~pO4^MJ2LqK3X;Irb%nf}L|gck zE#55_BNsy6m+W{e zo!P59DDo*s@VIi+S|v93PwY6d?CE=S&!JLXwE9{i)DMO*_X90;n2*mPDrL%{iqN!?%-_95J^L z=l<*{em(6|h7DR4+4G3Wr;4*}yrBkbe3}=p7sOW1xj!EZVKSMSd;QPw>uhKK z#>MlS@RB@-`ULv|#zI5GytO{=zp*R__uK~R6&p$q{Y{iNkg61yAgB8C^oy&``{~FK z8hE}H&nIihSozKrOONe5Hu?0Zy04U#0$fB7C6y~?8{or}KNvP)an=QP&W80mj&8WL zEZQF&*FhoMMG6tOjeiCIV;T{I>jhi9hiUwz?bkX3NS-k5eWKy)Mo_orMEg4sV6R6X&i-Q%JG;Esl+kLpn@Bsls9O|i9z`tKB^~1D5)RIBB&J<6T@a4$pUvh$IR$%ubH)joi z!7>ON0DPwx=>0DA>Bb^c?L8N0BBrMl#oDB+GOXJh;Y&6I)#GRy$W5xK%a;KS8BrER zX)M>Rdoc*bqP*L9DDA3lF%U8Yzb6RyIsW@}IKq^i7v&{LeIc=*ZHIbO68x=d=+0T( zev=DT9f|x!IWZNTB#N7}V4;9#V$%Wo0%g>*!MdLOEU>My0^gni9ocID{$g9ytD!gy zKRWT`DVN(lcYjR|(}f0?zgBa3SwunLfAhx><%u0uFkrdyqlh8_g zDKt#R6rA2(Vm2LW_>3lBNYKG_F{TEnnKWGGC15y&OebIRhFL4TeMR*v9i0wPoK#H< zu4){s4K&K)K(9~jgGm;H7lS7y_RYfS;&!Oj5*eqbvEcW^a*i67nevzOZxN6F+K~A%TYEtsAVsR z@J=1hc#Dgs7J2^FL|qV&#WBFQyDtEQ2kPO7m2`)WFhqAob)Y>@{crkil6w9VoA?M6 zADGq*#-hyEVhDG5MQj677XmcWY1_-UO40QEP&+D)rZoYv^1B_^w7zAvWGw&pQyCyx zD|ga$w!ODOxxGf_Qq%V9Z7Q2pFiUOIK818AGeZ-~*R zI1O|SSc=3Z?#61Rd|AXx2)K|F@Z1@x!hBBMhAqiU)J=U|Y)T$h3D?ZPPQgkSosnN! zIqw-t$0fqsOlgw3TlHJF*t$Q@bg$9}A3X=cS@-yU3_vNG_!#9}7=q7!LZ?-%U26W4 z$d>_}*s1>Ac%3uFR;tnl*fNlylJ)}r2^Q3&@+is3BIv<}x>-^_ng;jhdaM}6Sg3?p z0jS|b%QyScy3OQ(V*~l~bK>VC{9@FMuW_JUZO?y(V?LKWD6(MXzh}M3r3{7b4eB(#`(q1m{>Be%_<9jw8HO!x#yF6vez$c#kR+}s zZO-_;25Sxngd(}){zv?ccbLqRAlo;yog>4LH&uZUK1n>x?u49C)Y&2evH5Zgt~666 z_2_z|H5AO5Iqxv_Bn~*y1qzRPcob<+Otod5Xd2&z=C;u+F}zBB@b^UdGdUz|s!H}M zXG%KiLzn3G?FZgdY&3pV$nSeY?ZbU^jhLz9!t0K?ep}EFNqR1@E!f*n>x*!uO*~JF zW9UXWrVgbX1n#76_;&0S7z}(5n-bqnII}_iDsNqfmye@)kRk`w~1 z6j4h4BxcPe6}v)xGm%=z2#tB#^KwbgMTl2I*$9eY|EWAHFc3tO48Xo5rW z5oHD!G4kb?MdrOHV=A+8ThlIqL8Uu+7{G@ zb)cGBm|S^Eh5= z^E^SZ=yeC;6nNCdztw&TdnIz}^Of@Ke*@vjt)0g>Y!4AJvWiL~e7+9#Ibhe)> ziNwh>gWZL@FlWc)wzihocz+%+@*euwXhW%Hb>l7tf8aJe5_ZSH1w-uG|B;9qpcBP0 zM`r1Hu#htOl)4Cl1c7oY^t0e4Jh$-I(}M5kzWqh{F=g&IM#JiC`NDSd@BCKX#y<P@Gwl$3a3w z6<(b|K(X5FIR22M)sy$4jY*F4tT{?wZRI+KkZFb<@j@_C316lu1hq2hA|1wCmR+S@ zRN)YNNE{}i_H`_h&VUT5=Y(lN%m?%QX;6$*1P}K-PcPx>*S55v)qZ@r&Vcic-sjkm z! z=nfW&X`}iAqa_H$H%z3Tyz5&P3%+;93_0b;zxLs)t#B|up}JyV$W4~`8E@+BHQ+!y zuIo-jW!~)MN$2eHwyx-{fyGjAWJ(l8TZtUp?wZWBZ%}krT{f*^fqUh+ywHifw)_F> zp76_kj_B&zFmv$FsPm|L7%x-j!WP>_P6dHnUTv!9ZWrrmAUteBa`rT7$2ixO;ga8U z3!91micm}{!Btk+I%pMgcKs?H4`i+=w0@Ws-CS&n^=2hFTQ#QeOmSz6ttIkzmh^`A zYPq)G1l3h(E$mkyr{mvz*MP`x+PULBn%CDhltKkNo6Uqg!vJ#DA@BIYr9TQ`18Un2 zv$}BYzOQuay9}w(?JV63F$H6WmlYPPpH=R|CPb%C@BCv|&Q|&IcW7*LX?Q%epS z`=CPx{1HnJ9_46^=0VmNb>8JvMw-@&+V8SDLRYsa>hZXEeRbtf5eJ>0@Ds47zIY{N z42EOP9J8G@MXXdeiPx#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91AfN*P1ONa40RR91AOHXW0IY^$^8f$?lu1NER9Fe^SItioK@|V(ZWmgL zZT;XwPgVuWM>O%^|Dc$VK;n&?9!&g5)aVsG8cjs5UbtxVVnQNOV~7Mrg3+jnU;rhE z6fhW6P)R>_eXrXo-RW*y6RQ_qcb^s1wTu$TwriZ`=JUws>vRi}5x}MW1MR#7p|gIWJlaLK;~xaN}b< z<-@=RX-%1mt`^O0o^~2=CD7pJ<<$Rp-oUL-7PuG>do^5W_Mk#unlP}6I@6NPxY`Q} zuXJF}!0l)vwPNAW;@5DjPRj?*rZxl zwn;A(cFV!xe^CUu+6SrN?xe#mz?&%N9QHf~=KyK%DoB8HKC)=w=3E?1Bqj9RMJs3U z5am3Uv`@+{jgqO^f}Lx_Jp~CoP3N4AMZr~4&d)T`R?`(M{W5WWJV^z~2B|-oih@h^ zD#DuzGbl(P5>()u*YGo*Och=oRr~3P1wOlKqI)udc$|)(bacG5>~p(y>?{JD7nQf_ z*`T^YL06-O>T(s$bi5v~_fWMfnE7Vn%2*tqV|?~m;wSJEVGkNMD>+xCu#um(7}0so zSEu7?_=Q64Q5D+fz~T=Rr=G_!L*P|(-iOK*@X8r{-?oBlnxMNNgCVCN9Y~ocu+?XA zjjovJ9F1W$Nf!{AEv%W~8oahwM}4Ruc+SLs>_I_*uBxdcn1gQ^2F8a*vGjgAXYyh? zWCE@c5R=tbD(F4nL9NS?$PN1V_2*WR?gjv3)4MQeizuH`;sqrhgykEzj z593&TGlm3h`sIXy_U<7(dpRXGgp0TB{>s?}D{fwLe>IV~exweOfH!qM@CV5kib!YA z6O0gvJi_0J8IdEvyP#;PtqP*=;$iI2t(xG2YI-e!)~kaUn~b{6(&n zp)?iJ`z2)Xh%sCV@BkU`XL%_|FnCA?cVv@h*-FOZhY5erbGh)%Q!Av#fJM3Csc_g zC2I6x%$)80`Tkz#KRA!h1FzY`?0es3t!rKDT5EjPe6B=BLPr7s0GW!if;Ip^!AmGW zL;$`Vdre+|FA!I4r6)keFvAx3M#1`}ijBHDzy)3t0gwjl|qC2YB`SSxFKHr(oY#H$)x{L$LL zBdLKTlsOrmb>T0wd=&6l3+_Te>1!j0OU8%b%N342^opKmT)gni(wV($s(>V-fUv@0p8!f`=>PxC|9=nu ze{ToBBj8b<{PLfXV$h8YPgA~E!_sF9bl;QOF{o6t&JdsX?}rW!_&d`#wlB6T_h;Xf zl{4Tz5>qjF4kZgjO7ZiLPRz_~U@k5%?=30+nxEh9?s78gZ07YHB`FV`4%hlQlMJe@J`+e(qzy+h(9yY^ckv_* zb_E6o4p)ZaWfraIoB2)U7_@l(J0O%jm+Or>8}zSSTkM$ASG^w3F|I? z$+eHt7T~04(_WfKh27zqS$6* zzyy-ZyqvSIZ0!kkSvHknm_P*{5TKLQs8S6M=ONuKAUJWtpxbL#2(_huvY(v~Y%%#~ zYgsq$JbLLprKkV)32`liIT$KKEqs$iYxjFlHiRNvBhxbDg*3@Qefw4UM$>i${R5uB zhvTgmqQsKA{vrKN;TSJU2$f9q=y{$oH{<)woSeV>fkIz6D8@KB zf4M%v%f5U2?<8B(xn}xV+gWP?t&oiapJhJbfa;agtz-YM7=hrSuxl8lAc3GgFna#7 zNjX7;`d?oD`#AK+fQ=ZXqfIZFEk{ApzjJF0=yO~Yj{7oQfXl+6v!wNnoqwEvrs81a zGC?yXeSD2NV!ejp{LdZGEtd1TJ)3g{P6j#2jLR`cpo;YX}~_gU&Gd<+~SUJVh+$7S%`zLy^QqndN<_9 zrLwnXrLvW+ew9zX2)5qw7)zIYawgMrh`{_|(nx%u-ur1B7YcLp&WFa24gAuw~& zKJD3~^`Vp_SR$WGGBaMnttT)#fCc^+P$@UHIyBu+TRJWbcw4`CYL@SVGh!X&y%!x~ zaO*m-bTadEcEL6V6*{>irB8qT5Tqd54TC4`h`PVcd^AM6^Qf=GS->x%N70SY-u?qr>o2*OV7LQ=j)pQGv%4~z zz?X;qv*l$QSNjOuQZ>&WZs2^@G^Qas`T8iM{b19dS>DaXX~=jd4B2u`P;B}JjRBi# z_a@&Z5ev1-VphmKlZEZZd2-Lsw!+1S60YwW6@>+NQ=E5PZ+OUEXjgUaXL-E0fo(E* zsjQ{s>n33o#VZm0e%H{`KJi@2ghl8g>a~`?mFjw+$zlt|VJhSU@Y%0TWs>cnD&61fW4e0vFSaXZa4-c}U{4QR8U z;GV3^@(?Dk5uc@RT|+5C8-24->1snH6-?(nwXSnPcLn#X_}y3XS)MI_?zQ$ZAuyg+ z-pjqsw}|hg{$~f0FzmmbZzFC0He_*Vx|_uLc!Ffeb8#+@m#Z^AYcWcZF(^Os8&Z4g zG)y{$_pgrv#=_rV^D|Y<_b@ICleUv>c<0HzJDOsgJb#Rd-Vt@+EBDPyq7dUM9O{Yp zuGUrO?ma2wpuJuwl1M=*+tb|qx7Doj?!F-3Z>Dq_ihFP=d@_JO;vF{iu-6MWYn#=2 zRX6W=`Q`q-+q@Db|6_a1#8B|#%hskH82lS|9`im0UOJn?N#S;Y0$%xZw3*jR(1h5s z?-7D1tnIafviko>q6$UyqVDq1o@cwyCb*})l~x<@s$5D6N=-Uo1yc49p)xMzxwnuZ zHt!(hu-Ek;Fv4MyNTgbW%rPF*dB=;@r3YnrlFV{#-*gKS_qA(G-~TAlZ@Ti~Yxw;k za1EYyX_Up|`rpbZ0&Iv#$;eC|c0r4XGaQ-1mw@M_4p3vKIIpKs49a8Ns#ni)G314Z z8$Ei?AhiT5dQGWUYdCS|IC7r z=-8ol>V?u!n%F*J^^PZ(ONT&$Ph;r6X;pj|03HlDY6r~0g~X#zuzVU%a&!fs_f|m?qYvg^Z{y?9Qh7Rn?T*F%7lUtA6U&={HzhYEzA`knx1VH> z{tqv?p@I(&ObD5L4|YJV$QM>Nh-X3cx{I&!$FoPC_2iIEJfPk-$;4wz>adRu@n`_y z_R6aN|MDHdK;+IJmyw(hMoDCFCQ(6?hCAG5&7p{y->0Uckv# zvooVuu04$+pqof777ftk<#42@KQ((5DPcSMQyzGOJ{e9H$a9<2Qi_oHjl{#=FUL9d z+~0^2`tcvmp0hENwfHR`Ce|<1S@p;MNGInXCtHnrDPXCKmMTZQ{HVm_cZ>@?Wa6}O zHsJc7wE)mc@1OR2DWY%ZIPK1J2p6XDO$ar`$RXkbW}=@rFZ(t85AS>>U0!yt9f49^ zA9@pc0P#k;>+o5bJfx0t)Lq#v4`OcQn~av__dZ-RYOYu}F#pdsl31C^+Qgro}$q~5A<*c|kypzd} ziYGZ~?}5o`S5lw^B{O@laad9M_DuJle- z*9C7o=CJh#QL=V^sFlJ0c?BaB#4bV^T(DS6&Ne&DBM_3E$S^S13qC$7_Z?GYXTpR@wqr70wu$7+qvf-SEUa5mdHvFbu^7ew!Z1a^ zo}xKOuT*gtGws-a{Tx}{#(>G~Y_h&5P@Q8&p!{*s37^QX_Ibx<6XU*AtDOIvk|^{~ zPlS}&DM5$Ffyu-T&0|KS;Wnaqw{9DB&B3}vcO14wn;)O_e@2*9B&0I_ zZz{}CMxx`hv-XouY>^$Y@J(_INeM>lIQI@I>dBAqq1)}?Xmx(qRuX^i4IV%=MF306 z9g)i*79pP%_7Ex?m6ag-4Tlm=Z;?DQDyC-NpUIb#_^~V_tsL<~5<&;Gf2N+p?(msn zzUD~g>OoW@O}y0@Z;RN)wjam`CipmT&O7a|YljZqU=U86 zedayEdY)2F#BJ6xvmW8K&ffdS*0!%N<%RB!2~PAT4AD*$W7yzHbX#Eja9%3aD+Ah2 zf#T;XJW-GMxpE=d4Y>}jE=#U`IqgSoWcuvgaWQ9j1CKzG zDkoMDDT)B;Byl3R2PtC`ip=yGybfzmVNEx{xi_1|Cbqj>=FxQc{g`xj6fIfy`D8fA z##!-H_e6o0>6Su&$H2kQTujtbtyNFeKc}2=|4IfLTnye#@$Au7Kv4)dnA;-fz@D_8 z)>irG$)dkBY~zX zC!ZXLy*L3xr6cb70QqfN#Q>lFIc<>}>la4@3%7#>a1$PU&O^&VszpxLC%*!m-cO{B z-Y}rQr4$84(hvy#R69H{H zJ*O#uJh)TF6fbXy;fZkk%X=CjsTK}o5N1a`d7kgYYZLPxsHx%9*_XN8VWXEkVJZ%A z1A+5(B;0^{T4aPYr8%i@i32h)_)|q?9vws)r+=5u)1YNftF5mknwfd*%jXA2TeP}Z zQ!m?xJ3?9LpPM?_A3$hQ1QxNbR&}^m z!F999s?p^ak#C4NM_x2p9FoXWJ$>r?lJ)2bG)sX{gExgLA2s5RwHV!h6!C~d_H||J z>9{E{mEv{Z1z~65Vix@dqM4ZqiU|!)eWX$mwS5mLSufxbpBqqS!jShq1bmwCR6 z4uBri7ezMeS6ycaXPVu(i2up$L; zjpMtB`k~WaNrdgM_R=e#SN?Oa*u%nQy01?()h4A(jyfeNfx;5o+kX?maO4#1A^L}0 zYNyIh@QVXIFiS0*tE}2SWTrWNP3pH}1Vz1;E{@JbbgDFM-_Mky^7gH}LEhl~Ve5PexgbIyZ(IN%PqcaV@*_`ZFb=`EjspSz%5m2E34BVT)d=LGyHVz@-e%9Ova*{5@RD;7=Ebkc2GP%pIP^P7KzKapnh`UpH?@h z$RBpD*{b?vhohOKf-JG3?A|AX|2pQ?(>dwIbWhZ38GbTm4AImRNdv_&<99ySX;kJ| zo|5YgbHZC#HYgjBZrvGAT4NZYbp}qkVSa;C-LGsR26Co+i_HM&{awuO9l)Ml{G8zD zs$M8R`r+>PT#Rg!J(K6T4xHq7+tscU(}N$HY;Yz*cUObX7J7h0#u)S7b~t^Oj}TBF zuzsugnst;F#^1jm>22*AC$heublWtaQyM6RuaquFd8V#hJ60Z3j7@bAs&?dD#*>H0SJaDwp%U~27>zdtn+ z|8sZzklZy$%S|+^ie&P6++>zbrq&?+{Yy11Y>@_ce@vU4ZulS@6yziG6;iu3Iu`M= zf3rcWG<+3F`K|*(`0mE<$89F@jSq;j=W#E>(R}2drCB7D*0-|D;S;(;TwzIJkGs|q z2qH{m_zZ+el`b;Bv-#bQ>}*VPYC|7`rgBFf2oivXS^>v<&HHTypvd4|-zn|=h=TG{ z05TH2+{T%EnADO>3i|CB zCu60#qk`}GW{n4l-E$VrqgZGbI zbQW690KgZt4U3F^5@bdO1!xu~p@7Y~*_FfWg2CdvED5P5#w#V46LH`<&V0{t&Ml~4 zHNi7lIa+#i+^Z6EnxO7KJQw)wD)4~&S-Ki8)3=jpqxmx6c&zU&<&h%*c$I(5{1HZT zc9WE}ijcWJiVa^Q^xC|WX0habl89qycOyeViIbi(LFsEY_8a|+X^+%Qv+W4vzj>`y zpuRnjc-eHNkvXvI_f{=*FX=OKQzT?bck#2*qoKTHmDe>CDb&3AngA1O)1b}QJ1Tun z_<@yVEM>qG7664Pa@dzL@;DEh`#?yM+M|_fQS<7yv|i*pw)|Z8)9IR+QB7N3v3K(wv4OY*TXnH&X0nQB}?|h2XQeGL^q~N7N zDFa@x0E(UyN7k9g%IFq7Sf+EAfE#K%%#`)!90_)Dmy3Bll&e1vHQyPA87TaF(xbqMpDntVp?;8*$87STop$!EAnGhZ?>mqPJ(X zFsr336p3P{PpZCGn&^LP(JjnBbl_3P3Kcq+m}xVFMVr1zdCPJMDIV_ki#c=vvTwbU z*gKtfic&{<5ozL6Vfpx>o2Tts?3fkhWnJD&^$&+Mh5WGGyO7fG@6WDE`tEe(8<;+q z@Ld~g08XDzF8xtmpIj`#q^(Ty{Hq>t*v`pedHnuj(0%L(%sjkwp%s}wMd!a<*L~9T z9MM@s)Km~ogxlqEhIw5(lc46gCPsSosUFsgGDr8H{mj%OzJz{N#;bQ;KkV+ZWA1(9 zu0PXzyh+C<4OBYQ0v3z~Lr;=C@qmt8===Ov2lJ1=DeLfq*#jgT{YQCuwz?j{&3o_6 zsqp2Z_q-YWJg?C6=!Or|b@(zxTlg$ng2eUQzuC<+o)k<6^9ju_Z*#x+oioZ5T8Z_L zz9^A1h2eFS0O5muq8;LuDKwOv4A9pxmOjgb6L*i!-(0`Ie^d5Fsgspon%X|7 zC{RRXEmYn!5zP9XjG*{pLa)!2;PJB2<-tH@R7+E1cRo=Wz_5Ko8h8bB$QU%t9#vol zAoq?C$~~AsYC|AQQ)>>7BJ@{Cal)ZpqE=gjT+Juf!RD-;U0mbV1ED5PbvFD6M=qj1 zZ{QERT5@(&LQ~1X9xSf&@%r|3`S#ZCE=sWD`D4YQZ`MR`G&s>lN{y2+HqCfvgcw3E z-}Kp(dfGG?V|97kAHQX+OcKCZS`Q%}HD6u*e$~Ki&Vx53&FC!x94xJd4F2l^qQeFO z?&JdmgrdVjroKNJx64C!H&Vncr^w zzR#XI}Dn&o8jB~_YlVM^+#0W(G1LZH5K^|uYT@KSR z^Y5>^*Bc45E1({~EJB(t@4n9gb-eT#s@@7)J^^<_VV`Pm!h7av8XH6^5zO zOcQBhTGr;|MbRsgxCW69w{bl4EW#A~);L?d4*y#j8Ne=Z@fmJP0k4{_cQ~KA|Y#_#BuUiYx8y*za3_6Y}c=GSe7(2|KAfhdzud!Zq&}j)=o4 z7R|&&oX7~e@~HmyOOsCCwy`AR+deNjZ3bf6ijI_*tKP*_5JP3;0d;L_p(c>W1b%sG zJ*$wcO$ng^aW0E(5ldckV9unU7}OB7s?Wx(761?1^&8tA5y0_(ieV>(x-e@}1`lWC z-YH~G$D>#ud!SxK2_Iw{K%92=+{4yb-_XC>ji&j7)1ofp(OGa4jjF;Hd*`6YQL+Jf zffg+6CPc8F@EDPN{Kn96yip;?g@)qgkPo^nVKFqY?8!=h$G$V=<>%5J&iVjwR!7H0 z$@QL|_Q81I;Bnq8-5JyNRv$Y>`sWl{qhq>u+X|)@cMlsG!{*lu?*H`Tp|!uv z9oEPU1jUEj@ueBr}%Y)7Luyi)REaJV>eQ{+uy4uh0ep0){t;OU8D*RZ& zE-Z-&=BrWQLAD^A&qut&4{ZfhqK1ZQB0fACP)=zgx(0(o-`U62EzTkBkG@mXqbjXm z>w`HNeQM?Is&4xq@BB(K;wv5nI6EXas)XXAkUuf}5uSrZLYxRCQPefn-1^#OCd4aO zzF=dQ*CREEyWf@n6h7(uXLNgJIwGp#Xrsj6S<^bzQ7N0B0N{XlT;`=m9Olg<>KL}9 zlp>EKTx-h|%d1Ncqa=wnQEuE;sIO-f#%Bs?g4}&xS?$9MG?n$isHky0caj za8W+B^ERK#&h?(x)7LLpOqApV5F>sqB`sntV%SV>Q1;ax67qs+WcssfFeF3Xk=e4^ zjR2^(%K1oBq%0%Rf!y&WT;lu2Co(rHi|r1_uW)n{<7fGc-c=ft7Z0Q}r4W$o$@tQF#i?jDBwZ8h+=SC}3?anUp3mtRVv9l#H?-UD;HjTF zQ*>|}e=6gDrgI9p%c&4iMUkQa4zziS$bO&i#DI$Wu$7dz7-}XLk%!US^XUIFf2obO zFCTjVEtkvYSKWB;<0C;_B{HHs~ax_48^Cml*mjfBC5*7^HJZiLDir(3k&BerVIZF8zF;0q80eX8c zPN4tc+Dc5DqEAq$Y3B3R&XPZ=AQfFMXv#!RQnGecJONe0H;+!f^h5x0wS<+%;D}MpUbTNUBA}S2n&U59-_5HKr{L^jPsV8B^%NaH|tUr)mq=qCBv_- ziZ1xUp(ZzxUYTCF@C}To;u60?RIfTGS?#JnB8S8@j`TKPkAa)$My+6ziGaBcA@){d z91)%+v2_ba7gNecdj^8*I4#<11l!{XKl6s0zkXfJPxhP+@b+5ev{a>p*W-3*25c&} zmCf{g9mPWVQ$?Sp*4V|lT@~>RR)9iNdN^7KT@>*MU3&v^3e?=NTbG9!h6C|9zO097 zN{Qs6YwR-5$)~ z`b~qs`a1Dbx8P>%V=1XGjBptMf%P~sl1qbHVm1HYpY|-Z^Dar8^HqjIw}xaeRlsYa zJ_@Apy-??`gxPmb`m`0`z`#G7*_C}qiSZe~l2z65tE~IwMw$1|-u&t|z-8SxliH00 zlh1#kuqB56s+E&PWQ7Nz17?c}pN+A@-c^xLqh(j;mS|?>(Pf7(?qd z5q@jkc^nA&!K-}-1P=Ry0yyze0W!+h^iW}7jzC1{?|rEFFWbE^Yu7Y}t?jmP-D$f+ zmqFT7nTl0HL|4jwGm7w@a>9 zKD)V~+g~ysmei$OT5}%$&LK8?ib|8aY|>W3;P+0B;=oD=?1rg+PxKcP(d;OEzq1CKA&y#boc51P^ZJPPS)z5 zAZ)dd2$glGQXFj$`XBBJyl2y-aoBA8121JC9&~|_nY>nkmW>TLi%mWdn-^Jks-Jv| zSR*wij;A3Fcy8KsDjQ15?Z9oOj|Qw2;jgJiq>dxG(2I2RE- z$As!#zSFIskebqU2bnoM^N<4VWD2#>!;saPSsY8OaCCQqkCMdje$C?Sp%V}f2~tG5 z0whMYk6tcaABwu*x)ak@n4sMElGPX1_lmv@bgdI2jPdD|2-<~Jf`L`@>Lj7{<-uLQ zE3S_#3e10q-ra=vaDQ42QUY^@edh>tnTtpBiiDVUk5+Po@%RmuTntOlE29I4MeJI?;`7;{3e4Qst#i-RH6s;>e(Sc+ubF2_gwf5Qi%P!aa89fx6^{~A*&B4Q zKTF|Kx^NkiWx=RDhe<{PWXMQ;2)=SC=yZC&mh?T&CvFVz?5cW~ritRjG2?I0Av_cI z)=s!@MXpXbarYm>Kj0wOxl=eFMgSMc?62U#2gM^li@wKPK9^;;0_h7B>F>0>I3P`{ zr^ygPYp~WVm?Qbp6O3*O2)(`y)x>%ZXtztz zMAcwKDr=TCMY!S-MJ8|2MJCVNUBI0BkJV6?(!~W!_dC{TS=eh}t#X+2D>Kp&)ZN~q zvg!ogxUXu^y(P*;Q+y_rDoGeSCYxkaGPldDDx)k;ocJvvGO#1YKoQLHUf2h_pjm&1 zqh&!_KFH03FcJvSdfgUYMp=5EpigZ*8}7N_W%Ms^WSQ4hH`9>3061OEcxmf~TcYn5_oHtscWn zo5!ayj<_fZ)vHu3!A!7M;4y1QIr8YGy$P2qDD_4+T8^=^dB6uNsz|D>p~4pF3Nrb6 zcpRK*($<~JUqOya#M1=#IhOZ zG)W+rJS-x(6EoVz)P zsSo>JtnChdj9^);su%SkFG~_7JPM zEDz3gk2T7Y%x>1tWyia|op(ilEzvAujW?Xwlw>J6d7yEi8E zv30riR|a_MM%ZZX&n!qm0{2agq(s?x9E@=*tyT$nND+{Djpm7Rsy!+c$j+wqMwTOF zZL8BQ|I`<^bGW)5apO{lh(Asqen?_U`$_n0-Ob~Yd%^89oEe%9yGumQ_8Be+l2k+n zCxT%s?bMpv|AdWP7M1LQwLm|x+igA~;+iK-*+tClF&ueX_V}>=4gvZ01xpubQWXD_ zi?Un>&3=$fu)dgk-Z;0Ll}HK5_YM->l^Czrd0^cJ))(DwL2g3aZuza7ga9^|mT_70 z))}A}r1#-(9cxtn<9jGRwOB4hb9kK@YCgjfOM-90I$8@l=H^`K$cyhe2mTM|FY9vW znH~h)I<_aa#V1xmhk?Ng@$Jw-s%a!$BI4Us+Df+?J&gKAF-M`v}j`OWKP3>6`X`tEmhe#y*(Xm$_^Ybbs=%;L7h zp7q^C*qM}Krqsinq|WolR99>_!GL#Z71Hhz|IwQQv<>Ds09B?Je(lhI1(FInO8mc} zl$RyKCUmfku+Cd^8s0|t+e}5g7M{ZPJQH=UB3(~U&(w#Bz#@DTDHy>_UaS~AtN>4O zJ-I#U@R($fgupHebcpuEBX`SZ>kN!rW$#9>s{^3`86ZRQRtYTY)hiFm_9wU3c`SC8 z-5M%g)h}3Pt|wyj#F%}pGC@VL`9&>9P+_UbudCkS%y2w&*o})hBplrB*@Z?gel5q+ z%|*59(sR9GMk3xME}wd%&k?7~J)OL`rK#4d-haC7uaU8-L@?$K6(r<0e<;y83rK&` z3Q!1rD9WkcB8WBQ|WT|$u^lkr0UL4WH4EQTJyk@5gzHb18cOte4w zS`fLv8q;PvAZyY;*Go3Qw1~5#gP0D0ERla6M6#{; zr1l?bR}Nh+OC7)4bfAs(0ZD(axaw6j9v`^jh5>*Eo&$dAnt?c|Y*ckEORIiJXfGcM zEo`bmIq6rJm`XhkXR-^3d8^RTK2;nmVetHfUNugJG(4XLOu>HJA;0EWb~?&|0abr6 zxqVp@p=b3MN^|~?djPe!=eex(u!x>RYFAj|*T$cTi*Sd3Bme7Pri1tkK9N`KtRmXf zZYNBNtik97ct1R^vamQBfo9ZUR@k*LhIg8OR9d_{iv#t)LQV91^5}K5u{eyxwOFoU zHMVq$C>tfa@uNDW^_>EmO~WYQd(@!nKmAvSSIb&hPO|}g-3985t?|R&WZXvxS}Kt2i^eRe>WHb_;-K5cM4=@AN1>E&1c$k!w4O*oscx(f=<1K6l#8Exi)U(ZiZ zdr#YTP6?m1e1dOKysUjQ^>-MR={OuD00g6+(a^cvcmn#A_%Fh3Of%(qP5nvjS1=(> z|Ld8{u%(J}%2SY~+$4pjy{()5HN2MYUjg1X9umxOMFFPdM+IwOVEs4Z(olynvT%G) zt9|#VR}%O2@f6=+6uvbZv{3U)l;C{tuc zZ{K$rut=eS%3_~fQv^@$HV6#9)K9>|0qD$EV2$G^XUNBLM|5-ZmFF!KV)$4l^KVj@ zZ4fI}Knv*K%zPqK77}B-h_V{66VrmoZP2>@^euu8Rc}#qwRwt5uEBWcJJE5*5rT2t zA4Jpx`QQ~1Sh_n_a9x%Il!t1&B~J6p54zxAJx`REov${jeuL8h8x-z=?qwMAmPK5i z_*ES)BW(NZluu#Bmn1-NUKQip_X&_WzJy~J`WYxEJQ&Gu7DD< z&F9urE;}8S{x4{yB zaq~1Zrz%8)<`prSQv$eu5@1RY2WLu=waPTrn`WK%;G5(jt^FeM;gOdvXQjYhax~_> z{bS_`;t#$RYMu-;_Dd&o+LD<5Afg6v{NK?0d8dD5ohAN?QoocETBj?y{MB)jQ%UQ}#t3j&iL!qr@#6JEajR3@^k5wgLfI9S9dT2^f`2wd z%I#Q*@Ctk@w=(u)@QC}yBvUP&fFRR-uYKJ){Wp3&$s(o~W7OzgsUIPx0|ph2L1(r*_Pa@T@mcH^JxBjh09#fgo|W#gG7}|)k&uD1iZxb0 z@|Y)W79SKj9sS&EhmTD;uI#)FE6VwQ*YAr&foK$RI5H8_ripb$^=;U%gWbrrk4!5P zXDcyscEZoSH~n6VJu8$^6LE6)>+=o#Q-~*jmob^@191+Ot1w454e3)WMliLtY6~^w zW|n#R@~{5K#P+(w+XC%(+UcOrk|yzkEes=!qW%imu6>zjdb!B#`efaliKtN}_c!Jp zfyZa`n+Nx8;*AquvMT2;c8fnYszdDA*0(R`bsof1W<#O{v%O!1IO4WZe=>XBu_D%d zOwWDaEtX%@B>4V%f1+dKqcXT>m2!|&?}(GK8e&R=&w?V`*Vj)sCetWp9lr@@{xe6a zE)JL&;p}OnOO}Nw?vFyoccXT*z*?r}E8{uPtd;4<(hmX;d$rqJhEF}I+kD+m(ke;J z7Cm$W*CSdcD=RYEBhedg>tuT{PHqwCdDP*NkHv4rvQTXkzEn*Mb0oJz&+WfWIOS4@ zzpPJ|e%a-PIwOaOC7uQcHQ-q(SE(e@fj+7oC@34wzaBNaP;cw&gm{Z8yYX?V(lIv5 zKbg*zo1m5aGA4^lwJ|bAU=j3*d8S{vp!~fLFcK8s6%Ng55_qW_d*3R%e=34aDZPfD z&Le39j|ahp6E7B0*9OVdeMNrTErFatiE+=Z!XZ^tv0y%zZKXRTBuPyP&C{5(H?t)S zKV24_-TKpOmCPzU&by8R1Q5HY^@IDoeDA9MbgizgQ*F1Er~HVmvSU>vx}pZVQ&tr| zOtZl8vfY2#L<)gZ=ba&wG~EI*Vd?}lRMCf+!b5CDz$8~be-HKMo5omk$w7p4`Mym*IR8WiTz4^kKcUo^8Hkcsu14u z`Pkg`#-Y^A%CqJ0O@UF|caAulf68@(zhqp~YjzInh7qSN7Ov%Aj(Qz%{3zW|xubJ- ztNE_u_MO7Q_585r;xD?e=Er}@U1G@BKW5v$UM((eByhH2p!^g9W}99OD8VV@7d{#H zv)Eam+^K(5>-Ot~U!R$Um3prQmM)7DyK=iM%vy>BRX4#aH7*oCMmz07YB(EL!^%F7?CA#>zXqiYDhS;e?LYPTf(bte6B ztrfvDXYG*T;ExK-w?Knt{jNv)>KMk*sM^ngZ-WiUN;=0Ev^GIDMs=AyLg2V@3R z7ugNc45;4!RPxvzoT}3NCMeK$7j#q3r_xV(@t@OPRyoKBzHJ#IepkDsm$EJRxL)A* zf{_GQYttu^OXr$jHQn}zs$Eh|s|Z!r?Yi+bS-bi+PE*lH zo|6ztu6$r_?|B~S#m>imI!kQP9`6X426uHRri!wGcK;J;`%sFM(D#*Le~W*t2uH`Q z(HEO9-c_`mhA@4QhbW+tgtt9Pzx=_*3Kh~TB$SKmU4yx-Ay&)n%PZPKg#rD4H{%Ke zdMY@rf5EAFfqtrf?Vmk&N(_d-<=bvfOdPrYwY*;5%j@O6@O#Qj7LJTk-x3LN+dEKy+X z>~U8j3Ql`exr1jR>+S4nEy+4c2f{-Q!3_9)yY758tLGg7k^=nt<6h$YE$ltA+13S<}uOg#XHe6 zZHKdNsAnMQ_RIuB;mdoZ%RWpandzLR-BnjN2j@lkBbBd+?i ze*!5mC}!Qj(Q!rTu`KrRRqp22c=hF6<^v&iCDB`n7mHl;vdclcer%;{;=kA(PwdGG zdX#BWoC!leBC4);^J^tPkPbIe<)~nYb6R3u{HvC!NOQa?DC^Q`|_@ zcz;rk`a!4rSLAS>_=b@g?Yab4%=J3Cc7pRv8?_rHMl_aK*HSPU%0pG2Fyhef_biA!aW|-(( z*RIdG&Lmk(=(nk28Q1k1Oa$8Oa-phG%Mc6dT3>JIylcMMIc{&FsBYBD^n@#~>C?HG z*1&FpYVvXOU@~r2(BUa+KZv;tZ15#RewooEM0LFb>guQN;Z0EBFMFMZ=-m$a3;gVD z)2EBD4+*=6ZF?+)P`z@DOT;azK0Q4p4>NfwDR#Pd;no|{q_qB!zk1O8QojE;>zhPu z1Q=1z^0MYHo1*``H3ex|bW-Zy==5J4fE2;g6sq6YcXMYK5i|S^9(OSw#v!3^!EB<% zZF~J~CleS`V-peStyf*I%1^R88D;+8{{qN6-t!@gTARDg^w2`uSzFZbPQ!)q^oC}m zPo8VOQxq2BaIN`pAVFGu8!{p3}(+iZ`f4ck2ygVpEZMQW38nLpj3NQx+&sAkb8`}P3- zc>N*k6AG?r}bfO6_vccTuKX+*- z7W4Q#2``P0jIHYs)F>uG#AM#I6W2)!Nu2nD5{CRV_PmkDS2ditmbd#pggqEgAo%5oC?|CP zGa0CV)wA*ko!xC7pZYkqo{10CN_e00FX5SjWkI3?@XG}}bze!(&+k2$C-C`6temSk z_YyYpB^wh3woo`B zrMSTd4T?(X-jh`FeO76C(3xsOm9s2BP_b%ospg^!#*2*o9N;tf4(X9$qc_d(()yz5 zDk@1}u_Xd+86vy5RBs?LQCuYKCGPS;E4uFOi@V%1JTK&|eRf~lp$AV#;*#O}iRI2=i3rFL8{ zA^ptDZ0l6k-mq=hUJ0x$Y@J>UNfz~I5l63H(`~*v;qX`Z{zwsQQD-!wp0D&hyB8&Z z7$R07gIKGJ^%AvQ{4KM0edM39iFRx=P^6`!<1(s0t|JbB2tXs_B_IH9#ajH0C=-n+ z`nz`fKMBKLlf?2AC+|83M+0rqR%uhNGD;uKA6jOjp7YDe^4%0fRB<^bcjlS2KF~F; zu09wh1x0&4pG&76M;x8$u`b134t=dEPBn6PV|X29<#T4F1mxGF*HOgiWU8tN@cguI z_F@o+XL7FJztR63wC|j4x_DANzcX94r7Iz-O2x$({&qd*mdLG=-Rv)uZ}UlMR+F&q zU}=lkfb0p1>1Ho){o$@}mSKIV;h*$AND7~Dl)QzpFBlSM99Kx+F7GsVK5xcR? z_4Q(Z%cgk8ST}U;;=!LwyZVu^S$>B-Waeik%wzcKTIqeX=0FP(TGQ=nxi=dsS5BYF zl@?}NT!Y!Iyos^@v7XWXA{_bV~1lxz7gC?xuXxy0_?GaN!AhRRM5>)^t%&ODd;@HN5L{MD3 zc>i2keQZVm#?NrDwbfd}_<*5^U&w0zv~n-y8=GGN-!=_`FU^cM8oVCWRFxw?BM^YD zi=Vxz4q|jwPTg+?q7_XI)-S@gQkh>w0ZUB}a{^ z_i;`Y(~fvpI!vmW*A^|P7(6+@C4UeL2WATf{P1?H5rk`5{TL zcf!CgP6Mi{MvjZS)rfo7JLDZK7M7ANd$3`{j9baD*7{#Zu-33fOYUzjvtKzR2)_T1I1s7fe&z|=)QkX;=`zX8!Byw-veM#yr;|wjO^II>!B*B z0+w%;0(=*G3V@88t!}~zx)&do(uF=073Yeh*fEhZb3Vn>t!m(9p~Y_FdV3IgR)9eT z)~e9xpI%2deTWyHlXA(7srrfc_`7ACm!R>SoIgkuF8 z!wkOhrixFy9y@)GdxAntd!!7@=L_tFD2T5OdSUO)I%yj02le`qeQ=yKq$g^h)NG;# za(0J@#VBi^5YI|QI=rq{KlxwGabZJ0dKmfWDROkcM}lUN$@DV`K7fU?8CP2H23QPi zG?YF*=Vn=kTK*#Y_{AQN&oLju|0#E=fx%YVh>S{puu&K$b;BN*jIo@VYhqPiJPzzM>#kxoy0vW9i;ne2_BIG0zyRFp<3M(iY(%*M_>q0ulV2K}Tg zkG{EWKS{i%4DUuHi%DVKy%e+Q!~Uf`>>F6NgD{{I8~nO4!VgOvtFOc7(O)X`|7n*f zxBa4CJ-v9fUUH+`7sPVvpM_C*udZ@OTGTzx56QM5y~OlrZc&w9=)B?nmd@keRn+^= zvm~4sa5987LFDnU{(N|N zJAR8H@}p1fC+H(yTI4n#%~TbImMpuqYn9cQ<0QQ%=PzZItLkC*ef9WJUvfITKWh#D zc#__8`4am9%#NslIUw+<82#SR8AYG|woLfBg#!-&dqq}@P>|I0%lbdy0lSMmNe+}o zj0zZuFr6Wb?Y{Qy-S=|r`bdrDmhnmvkRnkdn`YCleU>Q$=je}LGhh>_QAj6aa_0Oc z%Swsmui;IRx7bN*=AAS@5yW&Y2hy;3&|HAiA8}!HT6!Z!RVn~MZg`RmI6&%#tBZDx zfD+y@Z~NWlk*4l13vmt3AK2wP!fQlnBbECL>?p)F?T)<`w&QN>cP_V>r7UTcsTaaP zTOb$f!P@zf$6>890NVKbIkG8rE?9!Y97sMSZjfF?A zYR8lp`LMoz~O?iaZN;gcX;LC-%Ia*R%A&SLx!YIf29?P+=XAAojK8!^OU*@?R&DK!#G_lsn!#;S375uZ&B0HH1|BO0R90$U>qs zSvHv>H~mAgNCcjo-e+;RjY6B9NCbQrZ|BHjTkehaU<9CSkdd>Vl*ifA2LNOP&R2Qdy3k3-TQ+ zbq=#vI43x`s=%~cGyN&y4Y!FxhwgDe@i6uv8^BLL&3z*SO=D0aLjih?gY4-9uWp5or)H+v~w6n5X#F-I52z=Z_p4JB(;M| zeaVFhuR2|3UD2MzVc~^nSoD2(dD#uL_1PdnIxeA{V5n`#3xf1Zx@4lw(DsQ&H$h zw#%3O<1173hjg2_nhKi!d1ej=h7y`hVjCNB6|HTnx>SWuCE-kgTnfT+YGX4_Lun({ zDv2`>d3vrS)tTf7ps_vvh!Cx^e1BFuWnEAh0(7fkNk|-3oU|iRWdsC6U)?Raft~HN z;^$U}vZK5O8|LV$>6X5T(uYkblv{zwPxnQBh(BQ5tA~J!vGiAMYP^_ki~pkIxDfOZ zUJDwq%O~WueeV6%uN<54&u*c&E4y431cklBNrb06zGOOy4XNT~JS-q(s6@)F@ovbe ze`fial(O4(-su%6@@1+V0MsdLLMyE8;)nou(7}czU(5ASaZYDT(kUZ0L(&g$nF^n9 z9-Pi`ZZLX&)^*M6As4_2Mmc9S7OT)F8KkL2NJ)KJcnCuWU=Wy402A&45#Q9Id~BBH z0cY*xlv!uXzKrXLH!xQu(OtJvEj|0-DmRj1vjFz{c*I4$Pe(+_V|^b~S!0xm{8lq= zZv)@NlcyL3Xdz+*|L137F7y6L-2VsrKw=q^S>F6i%<{Fr8zk06$Ay-(!L$fY@7mcng!2}L0t zgi|KxfB63Xtk_Q8#ZPipQ@!zgjdpEIbK_?q17Hoi4Eiyun$hrc>T(7pOLVLQE=lgGwA+A308p& z7@=09(|$>eLy5gLe{*|3b(M;1n;C^~v?o88jYib48eR4$QGsBFzd}3QuwO^_XE(=B zq+hMi0UFC|dB{LCwch7;zYT=NK})O%sgi0k#yV;My@24^B1+CuZmYOh0^b)5Ba_)) zC%i#_Iev&nsu%I|1N5=MVc#PrlunKAs&hY|3s5;@}`>sB>}gzxuB zB=2vrRyB3uiyW(hkDUNe1@&(b`;>ZvGgw|@s{zVC#_`HXIN_^J@Etb zA7A+F?ot37T{<-vTy8h&b3e+WKHE1oh;pUQrN4yRRrx?mT_9jRa2i4l1fUnLW^Cbl z!I1>VzyFe?VELWWhM?@?t-YPZkD-Qjo@bC2(o#ZtZmr{KZsdFWItV`rs$gp{724@C zL8K5}E0+DHcWcL^{BGei4>@J-3%a#$y6;I}=upc};-NDv-z#kPX26ylOpH)Ov1uU{ zkLj6oiH6l_s+B~_z;|Jc2oi?naS7#3H63~~lWj4rUnd=fCnKdkik<@R&kch9q##G{ z4u!%=rlM~Yp3jk*t8}1B`Sv6<%Z^}~1e@aq zg|JQ`QO2pSjAm-g*?IrNc$^~sIrNBo2$m|Sxanr?Mfs>2@Auu49 zGXlsS<9XS1&8h(dD*Hl&5HBDG!^pJ*lkau_Ur+7`7z;rcs$hT4we?3bT=7Fe<>{5( z2m2(c+hUz2BTHM8dCe*Z3XX&Av;b~a=$6EF>&^E8%nyxO@m_n!q&XD^A{SRjRZQ0L~qDeC=j&0$j6=LNIz@`ni^>ch|sv}^6 zlm>?28yPl@WmDPR?Y-A9X{U9Dv_IsbXJnzKCjkRksLOg#42uG2mE_acbTQ4)J|1V>%U@K(FP3AYhL0U zdeOCPN1qLv!|#c=p!_+%VNV(GHt`RuLRV^vz<5tt-r)yOK**kUWPspVAf|}ZL{LS= z@k(@@!P&W!>wwe`x{+GrFSWhHov7hu?{KuuT%kl#WO@*WX$i_@retlhQBj++SVNCx z5$78LxP>Z=^aJ)D280r_jj=zFfMJFXCIe^B{~V@d1rl_F(qo&AB4bC-vYL>x2jSKX zpuTG-6kgp3e^T&+dtV*i6a~)v@n?n*MffN59y}<0djUX zt27R+SE#hp8bzc#;rk$jw3r4)Q@eI$*`_)=Pvge8@8|8>H3X)<9YX6cXa=ii#Le;(qKm@%0-7$>2ShnYc`j#zJ7gu_FE^?uAkL|H)UIH#gPu^40!6^J=^ zr`}iwa^!4tzW~vOMZAaKF>*8A{^8m$i(VK)>?=#l`xrVe>wseSvM_aF zATNkY>kM_P3?1kE`uIq#mvr-wuTgUH0N<&JhF=(E9%^NS*HLm!4GZ4_XI zL=R5tlG5Mk_1rPfg)sk^llFuKPMPBhuU|L5q#yP_mzxp1o&pAzi-X31sgFpIHn@($ z_>=`AB5(8tP6p2zS5VEvH5J$M` z_much3>S7t3Yo`Yx!>83-hW9LYzDKP?mKdkD#QAK8*M((sx{eBQdrR<^3ZhFP81+& zBnJMUefQyNBji~$5d88Wfw1Lv59aJN9t2!pABLg;ewJ#LXL-10;QcJl+Y4Mtngb)k6JZlCf)3uD_u)J3sYyN;NN5hNbg$%W!i-GK%e&!Us)2IExWSss$YG(hm3kJ-h%yD z>8q^n$+4I(_y_mbT{du4P%h1j3oSpjhY97{+IZ`aA4ug!vNJ6*p?<2H(2w+GD3j$I z1TUXGyNzdf>_yB3grP~FZUs<2Quw;eEi*7s(-MiIkQ%@J^+WGdQvYSUN+TRiD-xto zJ=OUU+kxGYc!HCLNbCvR4lGTp~#L;DFzGd-#gJe*xf(P3hDQz|y)?b9mwU3WUVnpcqXM<@w%r-k*Wr^gzAv)8T^sqA=Ye z!7qy&exJmAcAt~CwS#@yNmjr8*T*!A6w4~E*ibaLRs0CFo(;R3=ODhDt6zWNodmo0 zXx&bT$6&+5c>a|WJ)F4G-^GjY0H#*tY=UNyYr_q5fsrcjk(c^~e*7Lf`!Jd`)p412 zn|^*hV= zFI4UbwA%X@smDd$cQOiMC%jfitTxTb+#`9`G=2rJDfK!E=5ra|So>lc{X1$~w28i+ z4p&cTGwZ#5VueiXS9O8#;RR$yg7tL9!^)Sz&pZYIzlSh}0}V{LxL$Cu%B4U5_}k}- zm~|CsD<076x@<>m=6w6N?WaThIBP`!u{-;WF)xc=2otx*lwf|5+MkdJePjh(B z9SH+%cHGCMAXNxB{_3^otDWdsV7Ob6n{0 z+&!(;iaHOX__5z_$Qk{%xYV%Ig@7iokGBwR`3642ZP#H#v9QGbWl8<|MS*=@qO@Uj z6+SZ_v9`1paUe5tFN~v(b#J3a_Lx0+;r9giZIx-A5TxdbG>xi#AZ5_z1V}B^n)sxT zz49}eK7EWb6wR!6-qQOrHQHkUvshvq%=G2d&@(#XM*Am1;WbnJ{X_!a{ZkphD$^TQ z=Iskb&}=lBm(RHiwJoGg`*NiQ6#RB$T#LF+>#ef;Jne&MxKPX!#r`&TVEFsp2jnNx>dClzpcPy&G&13a_<0qaR3i+k212~hoQ z8nMk{JP-t04I{GW5gUBqcJW-jSMrlw}>p)ptx?WKuCUV77taMiV zHok9V=6yv+Uts@fMY&A}amC=!Yj}eL@=e%XJ#%?agkt1jWF+10{(E9mHLDa>Ll7Vj zG=3cp%ljIB-6pC}6&`xJ*6WCP|IlglLWJ^?yviI8Ve)?V_i4%n;olzny62_`-|IGi z^=}p_O>Z8M;c4|RExu70E7ePW(HWVS&E$+LL6xSQgB`QfMQJ|4pCTFowA39p5P-|$ zUtM_H2HnP8_RoS~Vwk(FhbG zH41licj%=0a;Ln2STFBvU}Ne&O&%8bYKj!h1FA#sNM`232fX|U3QPp#3C?mN2;hE9 z;)!@5ixSPl<89^7gwhHc2YAX1KJK$#*3`KOMIQ253q7-*RJ5k)zp9GBO|Ga~X*^}US5oN@aG&waHV%vi~r{t^`ptTxb zL}q1W8S7*>7oWwvgV4uFLZ(@k`R*=LO_|Gu`prs~!WQXj-NLIa^2(7IHg>BG^N zc|i{-^=&Cek9dkJFQys|sjG9i>LLz|;yCv{^1i%c*h>8zF91kLvS9HBQi~ZU!JL`B zK8N+U0fr1*6??Ium)AF!6tc1eGhXIYL6IRT7rmKp7+>?%5Pa6zC5)KY$ycF0ZJ`G5nEQDG100U-jLkH8^UE4g6wq?sg%pP=-$&G#bcN`^?w3a6 z((s$6eRKcSEIslW-kk5Qi|5Mg-(xdLF}PxxVh$PuO}#aR6pW1kV4Af!Bqh*btXNNZ z>-4(IUl+L4dw+3LcpGut=qB45O+W)Q5?*zZ2A6rJcg`qkSvWA!j^r2mqKuCm6`Py? z@^T#Ux04HemPGd!Hs7NkZdVn1}8_j`o?)*OKZGS!`ff)gF zG?v-lj$wWNWCcw2Mg2o18D~1?3_b0XzdiKBNkYSDpcv@&kp0POmweJE2ZkIQ3B!a! zIgIoE+Xv?;34kyo^QYjZk+tEqZvq^#QG(OzX4~X+KtsoQoddTWUR(yo8R+ObEF1j<-syWOb>)JQ&Zbdu(sctU%Mt zW&YR0{ttY2TTXYZ?~WNU&cES1Z2q(7SrWDh``!J(JM+Nk$!hu&Y;(7E`ZNKTe0w+% zJc?Qnw2B+%UR}0;cB0Rufa(7-3FF}?629@LgTiEC&2uyL6NxexOp?AKT^aAx3gi(W zao>r>MPw0eQ3>IV02uLsC@>yK_epX6GRg4{NEL2wPPF9=*L2RV3yyK8DhuEK>rmmV z`&Q~#c`lgR&93TdOCja|ewOXmPNRh7!&dMT(1ett#iDr8HZW~VqWW@7fe9B6;7S+? zbC`d4@MEau&mKlOPKd>*10q0c{~^baw6!a*w^sY#0Xim{oOsiXiDOhbG&kl3c$$n1 zMRrD83&QucDSEcV*7LIp8VTA@F<%qe+_c`L;6on(>SjAU^}5c9!BCffT>$VQhe=)z z8(=Ej{5>jhmjB3{xDfj2R@VmHQ!CqjlO4KnuOmvHy3K#po$yp_V;p_MKjh1`(rzj6 zHW956k1yvntz{_g?Xbs`avK(IjlTnsu%htO;D7 z?J#x^EzuvVn&NA=!MEj7cwe5A-Z$Zk2LBZH$~%E* zf`((xH0?`}hs|HA%mtwfOEsZJxxrennkTYcwP#FKO5%Lpc^JXhSpV|ZH$Wr;`}`_( zIP==gd3LYyVtwD|*ZJGi{7~x8{=^bGVqu0RJ`n_BZH9+}kz%-4ZRsImi@rx%=ZEKs zcPnUXo6hbJV>fH;@1|bAHIe0ijYI*&kdT|HkDS$9No9 zCHo=*HWb~U+Dtzxr+Esao}6@|;Pf+E$ay0$kQp#s{wlw+7aIKbMdf`OqhoG*;Tco0 zjrP}VQG#Y2cJuqoJg&5({)S(BA}q9T1lGeWRyu=Je|)I!6a+aj!IP^1({)ZYe&x6w zt3a)Dq^TB+A7CdB0-}#z2Ur$W&h3YVw8==!xONy$uQmDWh-@15iEOt!q2m&?ZLA|w z8loSb(0}7y6Xu0?M5Uf4>VZGluB`wMf2oh;m)ghxVda>3m}4%V)r^0nVQ5V6f3>*) z0&VN!N0~GC^P}vj$`EDMZEmVV;N&RISY2C;$0;2(<{Lt&PKzqRByQdiEHGAbwtbS zPj`Da5%U6k1oEtVzI}QNw;!hT6F+~|@=c@$C4NtO@=xgP?|5MyZAyuCzcvq4rdAv@C06%gZ`9%I);R6UGiGJobfux+<0DLS&|MSG4UH z_~o{^^9>ixMg~mY!-@Fai{xaE4^;qy9iZN15Gbn5ZqHWf>Jc5Rv6(#n8`1NcCsdmG zab*dSXVPaE?)wCalD;$ivF%@nB#7D`@YG04p6ed9m}4iJW|pfVMLE<-c{=-8$e?cH zUdU#mCj4gb zZKA^b9p*9S(}8@tw~1RNPHr7tQr;P+-)D8|sq=*o)G%RGqt> zzP5yf`pVxb)I51D_G~Xp^GNK zVI6sAX)a9s)e{8N3?35YA6aQTXuyszK3ah~CemzA&CII#8F&F#KN41~8I^&_%}6MCNb{W87qAF`zj_Y^szhb> z3p3}KbOxotY|(lD=;)`fYE_*{S}x;f^SW#)SU&5X#o|-R|trpa|L5PS5aa0 zTHw8%SDSVtU4?vyrhnq+^@dgFS)|(y{~(4j%3UEiO-rBM9%`)8(dh33pMLiuurNY# z#10AsQ7%*0Cu_DSAU}P;X(JwA64~Q_^R%d_zSm^6Aux?Pn70PM>9EvLeOX z&w9c)pGmcL22;MO3C_B>=NC0RJpMp8?#ZUf=GWRvy z6RHq3B}=MGVg?9@iKFBpsvnkVh3{Vpp=`CcD=u~@ql{my|6?3ssi3mCOPnjI&E}VC zc@X+Yl>;;DNo0W0`0th!X{?luDhOC{E8N=?!w}K1{V=)+1={m(f`Oc|N=07>}3;z{-(A zm{JL=j?Sro5iecmE2-pWlRf(r%|HEQ7kgwQ9+kt=NBhtQI7OwcZ#3%$Uf%^r2nhjY zoQ08MfC%_X{O9~WcirMZMhn#z^ux4Erx-tf-6bHD)9eH&^L>^jvAd^9A^DCDs?0;k zkm7LE*KjP6`2d17MrQaaLqd_Rka}J$csvUec#hw78<=s(hyR>065~YCVCA9+#Q+; za(*L0IEw!r5P|@-;x33L$Lv9 zcuN8YG&g{<(SeJG18~(b!5yywSqQiLAX0;---;}mF5&b4lg|T?LwKREa{9YX_-zL@ZE?Zqi@HxK^2KO1>0LATu{te=T zprmHtY)bDVfxI1S}KBE7V zznP7KQ8HekWU#W6mw`dr-boV}pMQR==&5=Q5T=_q091jfc;R*jX#&=MQ%~@E@9^?`$v48ks<>(fI(F6L(5ppKy|$HWng*bKOb(4|cMUB&z$#ob#XV z5-mg)gmFIybZf=znm3ZPyUO^GJfxt0kmHjaTZ|sthsxXw&}Y)fOUSg=JhRSR^UjZ- zhqqb}Wsyw4zdnj6@#BAJa#-PdI4_dgafFXh85DsEQ_cT+5)XpZq$fZlBA_9UsE9r6 zEFec5?uqN@QhJ^IzwZrwl-5J`CmVPv{(YDTqEqWR^dI;5hXc~cxP%B3v&~s0`Ct89 z@S`i~a^c%V^N81dDT*ItFS*&IN;@O$EgzX0e7x&}TD=!zS}hTpezBLS>mdX(5< z)8DEI(-o_D)c-UX@dA1MuJ*yc>Hf4|`*B2S_O>w*-tbUwtiu`;W(Ud{HTty@(&x(T(F&;M zJ=?H>6`B7nf-90e8V`WSVp|0oEKB-P2M{}4ZDawzvM&a!y>`Y#jCsD%T_l``@ah(I2nJs~Q|%uSKu@k!m~*8B*IoA{*TgtF<(5sHCGG;n@NE%~Xt(G$^&<87u;}Na zx-8cq0g`uA(&RBFo=-4Y1GUZ<``Zw{xL4jfHkZw~%~wvtGueszcXt)_QwH8g!; z%s&3kSa~R$dO$-%L-)c@_hi7&>{6L_M>OZFkUQu;{sL_bUMStNrt{{&O(Wn~*zPOk zB>dnfszb29NSTf2pqIs68k|p-UrSrxgLHqi?3N-UFa!LHy9n1)=s>`yS+J{MEzS@ zNlfGtpma7kG&LR3JE@wB%rFA*h~~KitlO=IP)ZjN6dQLM6qsry zHkB#cyNh#n`)}bCrN1My*;k)^@>e4gJ`LJK?2)Pwp?4Tl4)4FA0(tvY+#1jOUM)xw zlMz4x-f@g^+yKUN`?Vu)|AwujArnM~Pa@y*Q9S8eS(u{-S%(Z5=R~pRl5ZGDjdqH% zC8rW&{##wOpU_oTIG4WXMk4&%2t1;lWcW5&!yxmOT*!hBcKyTqEcNoO+R2;Q?Yj+W z1-Y4?59fijz4(MIDwGe4-baYf08UCs;r|YefD-Md2ST;=cxwpgW=tR76-dQVAhn^= zG9Wk5lQk%jIR@KNU!UMp6@BfU;r+;y4VQ)D2!Il9HX%yW-9nOzV+m$YKzVaO`B8S7t z$!S2Mz`xw>V(RjE`0>bQp<0y&h~Y=M#jpy!#=dE>`=e_AjSZq6u!Dy1xJf~-7|0F! zPR9|n`e_7D2DIV2H(CESQ}hA>U>n|6`%z?YKEA~)BOVY%y=jPV zT=44R!L?J)736X#csn|lfBJ)o8ixaZclguWgrGO<`TN2FMfO}7;5}d+BlK0yTSH3* z4!=;5rOh85&2|x=46hkNaz?)U8&=bcfh=N_#8BNpZ2v$aVBo;sk^*X`v;4-LU;D>! zM*h12MxXIQy)SfAqE4;jY)wgnppazZkdNNVVF;(PLf^qK$FgY9+VFyBKE7UC|f z`R|?&egV11K3s$rJ6!GvoeW=jV*!-e(wA;x(2=d0E_e_%0x--0o8#~m^H1%AH5Z^B zn!TNPn927*bvaf0pt}zhK0o^V@WlGwwKo(*nQ|Q~4_;>~-8y20`HP>@UJa)3nEnGG z5Hwhs|FcmFG16ZVNb5hL`2Gc1{zWIMM{_OiKewV!hCi}U!VuE?s9wU-QbZ!)+Y^tS zGzp5OSi5iq6hmEr$w}&9DFgoB+i*`q`8TBi^MVS{SKEb8Aw%@K7@XCo(De2A`6%mf&a2#~y1N)+kJLD$1HCP!22)(U}xo2|j?WRzt(11j8Z_*v;P$R+Ug*Gy3VxV4K; zGGUGabnW*`Z}~`ydXL-l9e=GC$pY#z|63vy>E*m=$=j}iWP{sRTh0%H54`t>2xYH% zsk+M&u&pNgMCM@3e)Xc?jBWX-TIR_cQ1Z!RW7!B zBjZX=+^3}?SE)B+$EP+0oi1Fp5blDT?*}nsP>filqXH{ms zxU<$hetC`u)Wi+x|EKL-`y^#aQX+sDYIa{M;V%LqLrOk~lR>u0Q!+pyQSU4zY`?E^ z|5@)C)w6G_=i5YYC5SE_u(7hDNYr}uKT|@DSqF%S++lTIbIk^$a>{~0IH8KNFEy%+ zW#$&!ynpgNJh>6uR~?2c)ZMW+h0OKu231(7L_vETPaR+(P)Zy%0~yGm>E9?@@x!Jy z3PYgS}Q@b}x}E#F27@F+j}0=&Ql4gES&f8acMrPAVlVs9$97`FR))R5wI zc&}KFI1UIewh>3PkhnB7u zS3AT8_*|nexznG|Z*DU0c!K@jsI4J)5#DyNi#|e#`l1Vv1`1)*NVcy0LZ``aL0n8B zecupJ(rhq3u8bW0NIRhKYq$v1li+jp*4hfAd&wxYDE8vn1TQ7S@bTM|I2Ob z8vMOIxA7&_j{AKmD+O@EyXT`|dElt0pED^@IV0m)RPBUs*5jW60>>w1!@_G3aBKzG z_f(KfAPBk}-jQtR*Sroq!*3rbQ_m27e+YdzQjUb<_*k8vc_C)y!@cj5E>NxUhPu&g z@Z2<~esU`)ih+4opWe+K7sbN9n*9@n>#@n3*o z?xoROgDuvhq>jJ;Ve{6i<3roQNfgo5^4Q4(|GNExO2Dr7GjgA2zWuKp_K)K0R(6lv z!l$!zW-+T6mb3gQaAFviTQi{|*t%>{(mhTdy+y;Re4qT@kccy#{b z&zWy~kLO@>*WPj2k#H)|7L&gAJ37DmHQAme#@m;(Y8Nu^`D5vf8sZFW#+lA2!HK=( zJ)#hO6JD*`o~&c*&46d}g=Qj@SsoB5ikC z^1V8E+&<-OzuS_C`p5<<(A6fB`LXT(!kV^0_~hL6PpW4={l%|#xgdh?5EIk~lu8{D z2hiyhv3Yxij_#$Wu>P@7SYsl`-~3;}Ktx{34_NL^Kwin&=?!HDv3elQDbcU*qyYpN z(#yw~f1vFGK-t%CC-qa-4FYHbA^h>bag-I&*qaxwn?Qv|idE$<>1H|Gr6JtUu(he2$eg!N z@HTF@dG1)*y;4fxe)4_ZkpaBHH9hXp9p4|gLrRQyuevRd@gSS}JhRnWqrvm|U@>qM z=yl7RQROTKwQtzP3!zUF)_6Ld#NGA6v~2{J9Dd`h6{%+XsU#qGLh%`fB1Hc?wfayK zN`H4BpDp)npVQuu$DVW1qsBS&AJ2eP%6Qw>;k{)Z$8%HL=Q4(a$Ng2_vHw&vA!1L+9zc8vaX2GtqJ{L-;gvF0IR$em zMQ8@{Qp3+3Quk)TJ$?I<8KmwzD*7#(q<@Mc`dchngW}cRG14(Z6K7{T|LhFXwhqUQ;BET;cYqPcAcMgt6M$V9$(?jHo@Sud$an$U&5F zZ1QNh^ztt)E*d#Ij;<43oSKKnd+WNr$_r}+s_O_x6DZSB10*5Q{ourqq>mTl| zx4y^(cy+9;t@R=*j>3_dmm_m)$k$#937V(sllby&5)Xex^UD-|m|q<(jEd#@DV(of zAd7sSdmS*zUDqJ9|K%O2J2OfdUiK{{b{PCy)pi<;hp~7v1CQj&4-10 zgO<3dqhYH1#-Fa}Q{pjql5>>P6gZH21zLfxZ4$SK4T@7b!|`nWF9b*84Bq8&Eht;9 z*P72x&NUCZ7*@B$`FtE=hz5b}S`|c6Ey+j@D1ZibjJaRlR;{cxAWv z?Nqa>QqV*H-*zzaPvpLMHt~nl(x6?vrPpR?zn7~wow?oj*1TKmx4j71>$hvtC$DLD zUrz0^tiP0792U&dxJxNv@r}Elsjn^aSLUu=9#mD{&9n8|ayIL$!H3s>%KEvbchBFW z%cd?VU83mGF#Dar9*s~w&AnmQRQIOvR+uWsuZ?+|a=TzApXO@q^(r%8=}iv#wCnFq z=K9}JbqU@k99Q%j-}NNk+qLCP)jXfmOO|)@?mHcnynd6({mJisP1_}u7k)|eYHXWK z63eQ)E$ufFi!3CWUY2gw%e>omCv}qEX66aH-k&35f9`Q@Us|NPetVqe8=dX*VxJdn ze`q7b=Dn(UA(2sf&g)cOmQFhNJ#<-aMELJZbA#@to>25@kbW<)&!X01 z%NMJt>1ST)tyX)h@?`DxhbgCHr>S4wv}WC&Nw-!{+Z7$2D}74QAcXTvip=M0%Tp_N zor=k`)t|ra^ySr-+(|R9mB(E=`MX#y(wSw)$!iymzB;^c*>%&^*7HxTnRga=soSZT zdDl+9s;r!v8hk6POtzBaig4pRp7eWF(<8gufvNHPu6xs-=e{;mnHzJyGKE+8L0j}; z@%8-e^UCL5HhMiR>sD3Rve&yVZ#{Q1*CO8c+qSr^Z#CN;)(X5>tGG5yUw3<+CfhaL z%bP;hZ?jvgJU67BWyiy74_)6r)_nSxttxn0`0?HE^5(uydHVgP+HE$V?Lv)Leti43 zWA|;f-RqX``95>)^P-fw!Vi{3KNsII-*5f){gdxqd%gVdB1sOBNe=nEW%;i~g_P8J w!5uhoe-Jcg1nPN%MiEAtgE$;km@@t6ukO)1^!cY^83Pb_y85}Sb4q9e0FIsP9{>OV literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000000000000000000000000000000000000..2f1632cfddf3d9dade342351e627a0a75609fb46 GIT binary patch literal 2218 zcmV;b2vzrqP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuE6iGxuRCodHTWf3-RTMruyW6Fu zQYeUM04eX6D5c0FCjKKPrco1(K`<0SL=crI{PC3-^hZU0kQie$gh-5!7z6SH6Q0J% zqot*`H1q{R5fHFYS}dje@;kG=v$L0(yY0?wY2%*c?A&{2?!D*x?m71{of2gv!$5|C z3>qG_BW}7K_yUcT3A5C6QD<+{aq?x;MAUyAiJn#Jv8_zZtQ{P zTRzbL3U9!qVuZzS$xKU10KiW~Bgdcv1-!uAhQxf3a7q+dU6lj?yoO4Lq4TUN4}h{N z*fIM=SS8|C2$(T>w$`t@3Tka!(r!7W`x z-isCVgQD^mG-MJ;XtJuK3V{Vy72GQ83KRWsHU?e*wrhKk=ApIYeDqLi;JI1e zuvv}5^Dc=k7F7?nm3nIw$NVmU-+R>> zyqOR$-2SDpJ}Pt;^RkJytDVXNTsu|mI1`~G7yw`EJR?VkGfNdqK9^^8P`JdtTV&tX4CNcV4 z&N06nZa??Fw1AgQOUSE2AmPE@WO(Fvo`%m`cDgiv(fAeRA%3AGXUbsGw{7Q`cY;1BI#ac3iN$$Hw z0LT0;xc%=q)me?Y*$xI@GRAw?+}>=9D+KTk??-HJ4=A>`V&vKFS75@MKdSF1JTq{S zc1!^8?YA|t+uKigaq!sT;Z!&0F2=k7F0PIU;F$leJLaw2UI6FL^w}OG&!;+b%ya1c z1n+6-inU<0VM-Y_s5iTElq)ThyF?StVcebpGI znw#+zLx2@ah{$_2jn+@}(zJZ{+}_N9BM;z)0yr|gF-4=Iyu@hI*Lk=-A8f#bAzc9f z`Kd6K--x@t04swJVC3JK1cHY-Hq+=|PN-VO;?^_C#;coU6TDP7Bt`;{JTG;!+jj(` zw5cLQ-(Cz-Tlb`A^w7|R56Ce;Wmr0)$KWOUZ6ai0PhzPeHwdl0H(etP zUV`va_i0s-4#DkNM8lUlqI7>YQLf)(lz9Q3Uw`)nc(z3{m5ZE77Ul$V%m)E}3&8L0 z-XaU|eB~Is08eORPk;=<>!1w)Kf}FOVS2l&9~A+@R#koFJ$Czd%Y(ENTV&A~U(IPI z;UY+gf+&6ioZ=roly<0Yst8ck>(M=S?B-ys3mLdM&)ex!hbt+ol|T6CTS+Sc0jv(& z7ijdvFwBq;0a{%3GGwkDKTeG`b+lyj0jjS1OMkYnepCdoosNY`*zmBIo*981BU%%U z@~$z0V`OVtIbEx5pa|Tct|Lg#ZQf5OYMUMRD>Wdxm5SAqV2}3!ceE-M2 z@O~lQ0OiKQp}o9I;?uxCgYVV?FH|?Riri*U$Zi_`V2eiA>l zdSm6;SEm6#T+SpcE8Ro_f2AwxzI z44hfe^WE3!h@W3RDyA_H440cpmYkv*)6m1XazTqw%=E5Xv7^@^^T7Q2wxr+Z2kVYr + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/Configs/AppInfo.xcconfig b/demos/supabase-todolist-new-attachment/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 00000000..370c9893 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = PowerSync Supabase Demo + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = co.powersync.demotodolist + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2023 Journey Mobile, Inc. All rights reserved. diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/Configs/Debug.xcconfig b/demos/supabase-todolist-new-attachment/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 00000000..36b0fd94 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/Configs/Release.xcconfig b/demos/supabase-todolist-new-attachment/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 00000000..dff4f495 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/Configs/Warnings.xcconfig b/demos/supabase-todolist-new-attachment/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 00000000..42bcbf47 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/DebugProfile.entitlements b/demos/supabase-todolist-new-attachment/macos/Runner/DebugProfile.entitlements new file mode 100644 index 00000000..08c3ab17 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + + diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/Info.plist b/demos/supabase-todolist-new-attachment/macos/Runner/Info.plist new file mode 100644 index 00000000..4789daa6 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/MainFlutterWindow.swift b/demos/supabase-todolist-new-attachment/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 00000000..2722837e --- /dev/null +++ b/demos/supabase-todolist-new-attachment/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/supabase-todolist-new-attachment/macos/Runner/Release.entitlements b/demos/supabase-todolist-new-attachment/macos/Runner/Release.entitlements new file mode 100644 index 00000000..ee95ab7e --- /dev/null +++ b/demos/supabase-todolist-new-attachment/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/demos/supabase-todolist-new-attachment/macos/RunnerTests/RunnerTests.swift b/demos/supabase-todolist-new-attachment/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..61f3bd1f --- /dev/null +++ b/demos/supabase-todolist-new-attachment/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/demos/supabase-todolist-new-attachment/pubspec.lock b/demos/supabase-todolist-new-attachment/pubspec.lock new file mode 100644 index 00000000..0e72a5e1 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/pubspec.lock @@ -0,0 +1,1061 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f + url: "https://pub.dev" + source: hosted + version: "85.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: b1ade5707ab7a90dfd519eaac78a7184341d19adb6096c68d499b59c7c6cf880 + url: "https://pub.dev" + source: hosted + version: "7.7.0" + app_links: + dependency: transitive + description: + name: app_links + sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba" + url: "https://pub.dev" + source: hosted + version: "6.4.0" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + camera: + dependency: "direct main" + description: + name: camera + sha256: dfa8fc5a1adaeb95e7a54d86a5bd56f4bb0e035515354c8ac6d262e35cec2ec8 + url: "https://pub.dev" + source: hosted + version: "0.10.6" + camera_android: + dependency: transitive + description: + name: camera_android + sha256: "08808be7e26fc3c7426c81b3fa387564b8e9c22e6fe9cb5675ce3ab7017d8203" + url: "https://pub.dev" + source: hosted + version: "0.10.10+3" + camera_avfoundation: + dependency: transitive + description: + name: camera_avfoundation + sha256: ca36181194f429eef3b09de3c96280f2400693f9735025f90d1f4a27465fdd72 + url: "https://pub.dev" + source: hosted + version: "0.9.19" + camera_platform_interface: + dependency: transitive + description: + name: camera_platform_interface + sha256: "2f757024a48696ff4814a789b0bd90f5660c0fb25f393ab4564fb483327930e2" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + camera_web: + dependency: transitive + description: + name: camera_web + sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f" + url: "https://pub.dev" + source: hosted + version: "0.3.5" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + url: "https://pub.dev" + source: hosted + version: "1.15.0" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e + url: "https://pub.dev" + source: hosted + version: "2.0.28" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + functions_client: + dependency: transitive + description: + name: functions_client + sha256: b410e4d609522357396cd84bb9a8f6e3a4561b5f7d3ce82267f6f1c2af42f16b + url: "https://pub.dev" + source: hosted + version: "2.4.2" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + gotrue: + dependency: transitive + description: + name: gotrue + sha256: "04a6efacffd42773ed96dc752f19bb20a1fbc383e81ba82659072b775cf62912" + url: "https://pub.dev" + source: hosted + version: "2.12.0" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" + http: + dependency: transitive + description: + name: http + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image: + dependency: "direct main" + description: + name: image + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + url: "https://pub.dev" + source: hosted + version: "4.5.4" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + jwt_decode: + dependency: transitive + description: + name: jwt_decode + sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb + url: "https://pub.dev" + source: hosted + version: "0.3.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + url: "https://pub.dev" + source: hosted + version: "10.0.9" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.dev" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + logging: + dependency: "direct main" + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mutex: + dependency: transitive + description: + name: mutex + sha256: "8827da25de792088eb33e572115a5eb0d61d61a3c01acbc8bcbe76ed78f1a1f2" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: "direct main" + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + url: "https://pub.dev" + source: hosted + version: "2.2.17" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + posix: + dependency: transitive + description: + name: posix + sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + postgrest: + dependency: transitive + description: + name: postgrest + sha256: "10b81a23b1c829ccadf68c626b4d66666453a1474d24c563f313f5ca7851d575" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + powersync: + dependency: "direct main" + description: + path: "../../packages/powersync" + relative: true + source: path + version: "1.14.1" + powersync_attachments_helper: + dependency: "direct overridden" + description: + path: "../../packages/powersync_attachments_helper" + relative: true + source: path + version: "0.6.18+10" + powersync_attachments_stream: + dependency: "direct main" + description: + path: "../../packages/powersync_attachments_stream" + relative: true + source: path + version: "0.0.1" + powersync_core: + dependency: "direct overridden" + description: + path: "../../packages/powersync_core" + relative: true + source: path + version: "1.4.1" + powersync_flutter_libs: + dependency: "direct overridden" + description: + path: "../../packages/powersync_flutter_libs" + relative: true + source: path + version: "0.4.9" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + realtime_client: + dependency: transitive + description: + name: realtime_client + sha256: "3a0a99b5bd0fc3b35e8ee846d9a22fa2c2117f7ef1cb73d1e5f08f6c3d09c4e9" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + retry: + dependency: transitive + description: + name: retry + sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" + url: "https://pub.dev" + source: hosted + version: "2.4.10" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e" + url: "https://pub.dev" + source: hosted + version: "2.7.5" + sqlite3_flutter_libs: + dependency: transitive + description: + name: sqlite3_flutter_libs + sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14" + url: "https://pub.dev" + source: hosted + version: "0.5.32" + sqlite3_web: + dependency: transitive + description: + name: sqlite3_web + sha256: "967e076442f7e1233bd7241ca61f3efe4c7fc168dac0f38411bdb3bdf471eb3c" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + sqlite_async: + dependency: "direct main" + description: + name: sqlite_async + sha256: "9332aedd311a19dd215dcb55729bc68dc587dc7655b569ab8819b68ee0be0082" + url: "https://pub.dev" + source: hosted + version: "0.11.7" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + storage_client: + dependency: transitive + description: + name: storage_client + sha256: "09bac4d75eea58e8113ca928e6655a09cc8059e6d1b472ee801f01fde815bcfc" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + supabase: + dependency: transitive + description: + name: supabase + sha256: f00172f5f0b2148ea1c573f52862d50cacb6f353f579f741fa35e51704845958 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + supabase_flutter: + dependency: "direct main" + description: + name: supabase_flutter + sha256: d88eccf9e46e57129725a08e72a3109b6f780921fdc27fe3d7669a11ae80906b + url: "https://pub.dev" + source: hosted + version: "2.9.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test: + dependency: "direct dev" + description: + name: test + sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e" + url: "https://pub.dev" + source: hosted + version: "1.25.15" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.dev" + source: hosted + version: "0.7.4" + test_core: + dependency: transitive + description: + name: test_core + sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" + url: "https://pub.dev" + source: hosted + version: "0.6.8" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + universal_io: + dependency: "direct main" + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" + url: "https://pub.dev" + source: hosted + version: "6.3.16" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" + url: "https://pub.dev" + source: hosted + version: "6.3.3" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + url: "https://pub.dev" + source: hosted + version: "15.0.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" + yet_another_json_isolate: + dependency: transitive + description: + name: yet_another_json_isolate + sha256: fe45897501fa156ccefbfb9359c9462ce5dec092f05e8a56109db30be864f01e + url: "https://pub.dev" + source: hosted + version: "2.1.0" +sdks: + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/demos/supabase-todolist-new-attachment/pubspec.yaml b/demos/supabase-todolist-new-attachment/pubspec.yaml new file mode 100644 index 00000000..1a46bd01 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/pubspec.yaml @@ -0,0 +1,34 @@ +name: powersync_flutter_demo_new +description: PowerSync Flutter Demo +publish_to: "none" + +version: 1.0.1 + +environment: + sdk: ^3.4.0 + +dependencies: + flutter: + sdk: flutter + # powersync_attachments_helper: ^0.6.18+10 + powersync_attachments_stream: + path: ../../packages/powersync_attachments_stream + powersync: ^1.14.1 + path_provider: ^2.1.1 + supabase_flutter: ^2.0.1 + path: ^1.8.3 + logging: ^1.2.0 + camera: ^0.10.5+7 + image: ^4.1.3 + universal_io: ^2.2.2 + sqlite_async: ^0.11.0 + +dev_dependencies: + flutter_test: + sdk: flutter + + flutter_lints: ^3.0.1 + test: ^1.25.15 + +flutter: + uses-material-design: true diff --git a/demos/supabase-todolist-new-attachment/web/favicon.png b/demos/supabase-todolist-new-attachment/web/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8aaa46ac1ae21512746f852a42ba87e4165dfdd1 GIT binary patch literal 917 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0X7 zltGxWVyS%@P(fs7NJL45ua8x7ey(0(N`6wRUPW#JP&EUCO@$SZnVVXYs8ErclUHn2 zVXFjIVFhG^g!Ppaz)DK8ZIvQ?0~DO|i&7O#^-S~(l1AfjnEK zjFOT9D}DX)@^Za$W4-*MbbUihOG|wNBYh(yU7!lx;>x^|#0uTKVr7USFmqf|i<65o z3raHc^AtelCMM;Vme?vOfh>Xph&xL%(-1c06+^uR^q@XSM&D4+Kp$>4P^%3{)XKjo zGZknv$b36P8?Z_gF{nK@`XI}Z90TzwSQO}0J1!f2c(B=V`5aP@1P1a|PZ!4!3&Gl8 zTYqUsf!gYFyJnXpu0!n&N*SYAX-%d(5gVjrHJWqXQshj@!Zm{!01WsQrH~9=kTxW#6SvuapgMqt>$=j#%eyGrQzr zP{L-3gsMA^$I1&gsBAEL+vxi1*Igl=8#8`5?A-T5=z-sk46WA1IUT)AIZHx1rdUrf zVJrJn<74DDw`j)Ki#gt}mIT-Q`XRa2-jQXQoI%w`nb|XblvzK${ZzlV)m-XcwC(od z71_OEC5Bt9GEXosOXaPTYOia#R4ID2TiU~`zVMl08TV_C%DnU4^+HE>9(CE4D6?Fz oujB08i7adh9xk7*FX66dWH6F5TM;?E2b5PlUHx3vIVCg!0Dx9vYXATM literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/web/icons/Icon-192.png b/demos/supabase-todolist-new-attachment/web/icons/Icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..b749bfef07473333cf1dd31e9eed89862a5d52aa GIT binary patch literal 5292 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 literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/web/icons/Icon-512.png b/demos/supabase-todolist-new-attachment/web/icons/Icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..88cfd48dff1169879ba46840804b412fe02fefd6 GIT binary patch literal 8252 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 literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/web/icons/Icon-maskable-192.png b/demos/supabase-todolist-new-attachment/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000000000000000000000000000000000000..eb9b4d76e525556d5d89141648c724331630325d GIT binary patch literal 5594 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 X>c!)3PgKfL&lX^$Z>Cpu&6)6jvi^Z! literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/web/icons/Icon-maskable-512.png b/demos/supabase-todolist-new-attachment/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..d69c56691fbdb0b7efa65097c7cc1edac12a6d3e GIT binary patch literal 20998 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 MPgg&ebxsLQ025IeI{*Lx literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/web/index.html b/demos/supabase-todolist-new-attachment/web/index.html new file mode 100644 index 00000000..b3b0c490 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/web/index.html @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + supabase_todolist + + + + + + + + + + diff --git a/demos/supabase-todolist-new-attachment/web/manifest.json b/demos/supabase-todolist-new-attachment/web/manifest.json new file mode 100644 index 00000000..9dcf6fe4 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "supabase_todolist", + "short_name": "supabase_todolist", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/demos/supabase-todolist-new-attachment/windows/.gitignore b/demos/supabase-todolist-new-attachment/windows/.gitignore new file mode 100644 index 00000000..d492d0d9 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/demos/supabase-todolist-new-attachment/windows/CMakeLists.txt b/demos/supabase-todolist-new-attachment/windows/CMakeLists.txt new file mode 100644 index 00000000..ccfc4498 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/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/supabase-todolist-new-attachment/windows/flutter/CMakeLists.txt b/demos/supabase-todolist-new-attachment/windows/flutter/CMakeLists.txt new file mode 100644 index 00000000..930d2071 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/demos/supabase-todolist-new-attachment/windows/flutter/generated_plugin_registrant.cc b/demos/supabase-todolist-new-attachment/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..691e6fc2 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,23 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#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( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/demos/supabase-todolist-new-attachment/windows/flutter/generated_plugin_registrant.h b/demos/supabase-todolist-new-attachment/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..dc139d85 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/demos/supabase-todolist-new-attachment/windows/flutter/generated_plugins.cmake b/demos/supabase-todolist-new-attachment/windows/flutter/generated_plugins.cmake new file mode 100644 index 00000000..0d5e9159 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/windows/flutter/generated_plugins.cmake @@ -0,0 +1,27 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + app_links + powersync_flutter_libs + sqlite3_flutter_libs + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/demos/supabase-todolist-new-attachment/windows/runner/CMakeLists.txt b/demos/supabase-todolist-new-attachment/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..394917c0 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/demos/supabase-todolist-new-attachment/windows/runner/Runner.rc b/demos/supabase-todolist-new-attachment/windows/runner/Runner.rc new file mode 100644 index 00000000..75674c07 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/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/supabase-todolist-new-attachment/windows/runner/flutter_window.cpp b/demos/supabase-todolist-new-attachment/windows/runner/flutter_window.cpp new file mode 100644 index 00000000..b25e363e --- /dev/null +++ b/demos/supabase-todolist-new-attachment/windows/runner/flutter_window.cpp @@ -0,0 +1,66 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/demos/supabase-todolist-new-attachment/windows/runner/flutter_window.h b/demos/supabase-todolist-new-attachment/windows/runner/flutter_window.h new file mode 100644 index 00000000..6da0652f --- /dev/null +++ b/demos/supabase-todolist-new-attachment/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/demos/supabase-todolist-new-attachment/windows/runner/main.cpp b/demos/supabase-todolist-new-attachment/windows/runner/main.cpp new file mode 100644 index 00000000..3eee96b7 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/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/supabase-todolist-new-attachment/windows/runner/resource.h b/demos/supabase-todolist-new-attachment/windows/runner/resource.h new file mode 100644 index 00000000..66a65d1e --- /dev/null +++ b/demos/supabase-todolist-new-attachment/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/demos/supabase-todolist-new-attachment/windows/runner/resources/app_icon.ico b/demos/supabase-todolist-new-attachment/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c04e20caf6370ebb9253ad831cc31de4a9c965f6 GIT binary patch literal 33772 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 literal 0 HcmV?d00001 diff --git a/demos/supabase-todolist-new-attachment/windows/runner/runner.exe.manifest b/demos/supabase-todolist-new-attachment/windows/runner/runner.exe.manifest new file mode 100644 index 00000000..a42ea768 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/demos/supabase-todolist-new-attachment/windows/runner/utils.cpp b/demos/supabase-todolist-new-attachment/windows/runner/utils.cpp new file mode 100644 index 00000000..f5bf9fa0 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/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/supabase-todolist-new-attachment/windows/runner/utils.h b/demos/supabase-todolist-new-attachment/windows/runner/utils.h new file mode 100644 index 00000000..3879d547 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/demos/supabase-todolist-new-attachment/windows/runner/win32_window.cpp b/demos/supabase-todolist-new-attachment/windows/runner/win32_window.cpp new file mode 100644 index 00000000..041a3855 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/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/supabase-todolist-new-attachment/windows/runner/win32_window.h b/demos/supabase-todolist-new-attachment/windows/runner/win32_window.h new file mode 100644 index 00000000..c86632d8 --- /dev/null +++ b/demos/supabase-todolist-new-attachment/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/packages/powersync_attachments_stream/lib/common.dart b/packages/powersync_attachments_stream/lib/common.dart new file mode 100644 index 00000000..14e01eb8 --- /dev/null +++ b/packages/powersync_attachments_stream/lib/common.dart @@ -0,0 +1,7 @@ +/// Platform-agnostic exports for all platforms (including web). +export 'src/attachment.dart'; +export 'src/sync_error_handler.dart'; +export 'src/abstractions/attachment_service.dart'; +export 'src/abstractions/attachment_context.dart'; +export 'src/abstractions/local_storage.dart'; +export 'src/abstractions/remote_storage.dart'; \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/powersync_attachments_stream.dart b/packages/powersync_attachments_stream/lib/powersync_attachments_stream.dart index 730bb4f4..c39485d8 100644 --- a/packages/powersync_attachments_stream/lib/powersync_attachments_stream.dart +++ b/packages/powersync_attachments_stream/lib/powersync_attachments_stream.dart @@ -1,6 +1,7 @@ -export 'src/attachment_service.dart'; -export 'src/attachment_context.dart'; -export 'src/attachment_state.dart'; -export 'src/local_storage.dart'; -export 'src/sync_error_handler.dart'; -export 'src/attachment_queue_service.dart'; \ No newline at end of file +/// Default exports for native platforms (dart:io). For web, use 'common.dart'. +export 'common.dart'; +export 'src/storage/io_local_storage.dart'; +export 'src/attachment_queue_service.dart'; +export 'src/implementations/attachment_service.dart'; +export 'src/implementations/attachment_context.dart'; +export 'src/abstractions/remote_storage.dart'; \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/src/abstractions/attachment_context.dart b/packages/powersync_attachments_stream/lib/src/abstractions/attachment_context.dart new file mode 100644 index 00000000..fc147375 --- /dev/null +++ b/packages/powersync_attachments_stream/lib/src/abstractions/attachment_context.dart @@ -0,0 +1,15 @@ +import '../attachment.dart'; + +abstract class AttachmentContext { + Future deleteAttachment(String id, dynamic tx); + Future ignoreAttachment(String id); + Future getAttachment(String id); + Future saveAttachment(Attachment attachment); + Future saveAttachments(List attachments); + Future> getAttachmentIds(); + Future> getAttachments(); + Future> getActiveAttachments(); + Future clearQueue(); + Future deleteArchivedAttachments(Future Function(List) callback); + Future upsertAttachment(Attachment attachment, dynamic context); +} \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/src/abstractions/attachment_service.dart b/packages/powersync_attachments_stream/lib/src/abstractions/attachment_service.dart new file mode 100644 index 00000000..a0c085ad --- /dev/null +++ b/packages/powersync_attachments_stream/lib/src/abstractions/attachment_service.dart @@ -0,0 +1,6 @@ +import 'attachment_context.dart'; + +abstract class AttachmentService { + Stream watchActiveAttachments(); + Future withContext(Future Function(AttachmentContext ctx) action); +} \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/src/abstractions/local_storage.dart b/packages/powersync_attachments_stream/lib/src/abstractions/local_storage.dart new file mode 100644 index 00000000..2049143f --- /dev/null +++ b/packages/powersync_attachments_stream/lib/src/abstractions/local_storage.dart @@ -0,0 +1,44 @@ +import 'dart:typed_data'; + +abstract class LocalStorage { + /// Saves binary data stream to storage at the specified file path + /// + /// [filePath] - Path where the file will be stored + /// [data] - Stream of binary data to store + /// Returns the total size of the written data in bytes + Future saveFile(String filePath, Stream data); + + /// Retrieves binary data stream from storage at the specified file path + /// + /// [filePath] - Path of the file to read + /// [mediaType] - Optional MIME type of the data + /// Returns a stream of binary data + Stream readFile(String filePath, {String? mediaType}); + + /// Deletes a file at the specified path + /// + /// [filePath] - Path of the file to delete + Future deleteFile(String filePath); + + /// Checks if a file exists at the specified path + /// + /// [filePath] - Path to check + /// Returns true if the file exists, false otherwise + Future fileExists(String filePath); + + /// Creates a directory at the specified path + /// + /// [path] - Path of the directory to create + Future makeDir(String path); + + /// Recursively removes a directory and its contents + /// + /// [path] - Path of the directory to remove + Future rmDir(String path); + + /// Copies a file from source to target path + /// + /// [sourcePath] - Path of the source file + /// [targetPath] - Path where the file will be copied + Future copyFile(String sourcePath, String targetPath); +} diff --git a/packages/powersync_attachments_stream/lib/src/abstractions/remote_storage.dart b/packages/powersync_attachments_stream/lib/src/abstractions/remote_storage.dart new file mode 100644 index 00000000..d954d7c4 --- /dev/null +++ b/packages/powersync_attachments_stream/lib/src/abstractions/remote_storage.dart @@ -0,0 +1,25 @@ +import 'dart:async'; +import '../attachment.dart'; + +/// Adapter for interfacing with remote attachment storage. +abstract class RemoteStorage { + /// Uploads a file to remote storage. + /// + /// [fileData] is a stream of byte arrays representing the file data. + /// [attachment] is the attachment record associated with the file. + Future uploadFile( + Stream> fileData, + Attachment attachment, + ); + + /// Downloads a file from remote storage. + /// + /// [attachment] is the attachment record associated with the file. + /// Returns a stream of byte arrays representing the file data. + Future>> downloadFile(Attachment attachment); + + /// Deletes a file from remote storage. + /// + /// [attachment] is the attachment record associated with the file. + Future deleteFile(Attachment attachment); +} diff --git a/packages/powersync_attachments_stream/lib/src/attachment.dart b/packages/powersync_attachments_stream/lib/src/attachment.dart new file mode 100644 index 00000000..065d7f5a --- /dev/null +++ b/packages/powersync_attachments_stream/lib/src/attachment.dart @@ -0,0 +1,162 @@ +import 'package:powersync_core/sqlite3_common.dart' show Row; +import 'package:powersync_core/powersync_core.dart'; + +enum AttachmentState { + /// The attachment is queued for download from the remote storage. + queuedDownload, + + /// The attachment is queued for upload to the remote storage. + queuedUpload, + + /// The attachment is queued for deletion from the remote storage. + queuedDelete, + + /// The attachment is fully synchronized with the remote storage. + synced, + + /// The attachment is archived and no longer actively synchronized. + archived, +} + +extension AttachmentStateX on AttachmentState { + static AttachmentState fromInt(int value) { + if (value < 0 || value >= AttachmentState.values.length) { + throw ArgumentError('Invalid value for AttachmentState: $value'); + } + return AttachmentState.values[value]; + } + + int toInt() => index; +} + +const defaultAttachmentsQueueTableName = 'attachments_queue'; + +class Attachment { + final String id; + final int timestamp; + final String filename; + final AttachmentState state; + final String? localUri; + final String? mediaType; + final int? size; + final bool hasSynced; + final String? metaData; + + const Attachment({ + required this.id, + this.timestamp = 0, + required this.filename, + this.state = AttachmentState.queuedDownload, + this.localUri, + this.mediaType, + this.size, + this.hasSynced = false, + this.metaData, + }); + + /// Factory constructor to create an Attachment from a database map. + /// [map] should be a `Map` from your database query. + // factory Attachment.fromMap(Map map) { + // return Attachment( + // id: map['id'] as String, + // timestamp: map['timestamp'] as int? ?? 0, + // filename: map['filename'] as String, + // localUri: map['local_uri'] as String?, + // mediaType: map['media_type'] as String?, + // size: map['size'] as int?, + // state: AttachmentStateX.fromInt(map['state'] as int), + // hasSynced: (map['has_synced'] as int? ?? 0) > 0, + // metaData: map['meta_data'] as String?, + // ); + // } + + factory Attachment.fromRow(Row row) { + return Attachment( + id: row['id'] as String, + timestamp: row['timestamp'] as int? ?? 0, + filename: row['filename'] as String, + localUri: row['local_uri'] as String?, + mediaType: row['media_type'] as String?, + size: row['size'] as int?, + // state: AttachmentState.queuedDownload, + state: AttachmentStateX.fromInt(row['state'] as int), + // state: AttachmentStateX.fromInt(row['state'] as int? ?? AttachmentState.queuedDownload.index), + // state: AttachmentState.values.firstWhere( + // (e) => e.index == row['state'], + // orElse: () => AttachmentState.queuedDownload, + // ), + hasSynced: (row['has_synced'] as int? ?? 0) > 0, + // metaData: 'test', + metaData: row['meta_data']?.toString(), + ); + } + + /// Returns a copy of this attachment with the given fields replaced. + Attachment copyWith({ + String? id, + int? timestamp, + String? filename, + AttachmentState? state, + String? localUri, + String? mediaType, + int? size, + bool? hasSynced, + String? metaData, + }) { + return Attachment( + id: id ?? this.id, + timestamp: timestamp ?? this.timestamp, + filename: filename ?? this.filename, + state: state ?? this.state, + localUri: localUri ?? this.localUri, + mediaType: mediaType ?? this.mediaType, + size: size ?? this.size, + hasSynced: hasSynced ?? this.hasSynced, + metaData: metaData ?? this.metaData, + ); + } + + /// Converts this Attachment to a JSON-compatible map. + Map toJson() { + try { + return { + 'id': id, + 'timestamp': timestamp, + 'filename': filename, + 'state': state.index, + 'local_uri': localUri, + 'media_type': mediaType, + 'size': size, + 'has_synced': hasSynced ? 1 : 0, + 'meta_data': metaData, + }; + } catch (e) { + print('toJson error: $e'); + return {}; + } + } +} + +class AttachmentsQueueTable extends Table { + AttachmentsQueueTable({ + String attachmentsQueueTableName = defaultAttachmentsQueueTableName, + List additionalColumns = const [], + List indexes = const [], + String? viewName, + }) : super.localOnly( + attachmentsQueueTableName, + [ + const Column.text('filename'), + const Column.text('local_uri'), + const Column.integer('timestamp'), + const Column.integer('size'), + const Column.text('media_type'), + const Column.integer('state'), + const Column.integer('has_synced'), + const Column.text('meta_data'), + ...additionalColumns, + ], + viewName: viewName, + indexes: indexes, + ); +} diff --git a/packages/powersync_attachments_stream/lib/src/attachment_context.dart b/packages/powersync_attachments_stream/lib/src/attachment_context.dart deleted file mode 100644 index 3b31c14b..00000000 --- a/packages/powersync_attachments_stream/lib/src/attachment_context.dart +++ /dev/null @@ -1,7 +0,0 @@ -class AttachmentContext { - // Used for transactional/exclusive operations - Future runExclusive(Future Function() action) async { - // Implement async mutex/lock here if needed - return await action(); - } -} \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart index 840251a4..27772b5d 100644 --- a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart +++ b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart @@ -1,66 +1,370 @@ -import 'attachment_service.dart'; -import 'attachment_state.dart'; -import 'attachment_context.dart'; -import 'local_storage.dart'; -import 'sync_error_handler.dart'; -import 'utils/mutex.dart'; import 'dart:async'; +import 'dart:typed_data'; +import 'package:logging/logging.dart'; +import 'package:sqlite_async/mutex.dart'; +import 'attachment.dart'; +import 'abstractions/attachment_service.dart'; +import 'abstractions/attachment_context.dart'; +import 'abstractions/local_storage.dart'; +import 'abstractions/remote_storage.dart'; +import 'sync_error_handler.dart'; +import 'implementations/attachment_service.dart'; +import 'sync/syncing_service.dart'; + +/// A watched attachment record item. +/// This is usually returned from watching all relevant attachment IDs. +class WatchedAttachmentItem { + final String id; + final String? fileExtension; + final String? filename; + final String? metaData; + + WatchedAttachmentItem({ + required this.id, + this.fileExtension, + this.filename, + this.metaData, + }) : assert( + fileExtension != null || filename != null, + 'Either fileExtension or filename must be provided.', + ); +} -class AttachmentQueueService implements AttachmentService { +/// Class used to implement the attachment queue. +/// Requires a database client, remote storage adapter, and an attachment directory name. +class AttachmentQueue { + final dynamic db; + final RemoteStorage remoteStorage; + final String attachmentsDirectory; + final Stream> Function() watchAttachments; final LocalStorage localStorage; - final SyncErrorHandler errorHandler; - final AsyncMutex _mutex = AsyncMutex(); + final String attachmentsQueueTableName; + final SyncErrorHandler? errorHandler; + final Duration syncInterval; + final int archivedCacheLimit; + final Duration syncThrottleDuration; + final List? subdirectories; + final bool downloadAttachments; + final Logger logger; - final StreamController> _activeAttachmentsController = StreamController.broadcast(); - final List _attachments = []; + static const String defaultTableName = 'attachments_queue'; - AttachmentQueueService({ + final Mutex _mutex = Mutex(); + bool _closed = false; + StreamSubscription? _syncStatusSubscription; + late final AttachmentService attachmentsService; + late final SyncingService syncingService; + + AttachmentQueue({ + required this.db, + required this.remoteStorage, + required this.attachmentsDirectory, + required this.watchAttachments, required this.localStorage, - required this.errorHandler, - }); + this.attachmentsQueueTableName = defaultTableName, + this.errorHandler, + this.syncInterval = const Duration(seconds: 30), + this.archivedCacheLimit = 100, + this.syncThrottleDuration = const Duration(seconds: 1), + this.subdirectories, + this.downloadAttachments = true, + Logger? logger, + }) : logger = logger ?? Logger('AttachmentQueue') { + attachmentsService = AttachmentServiceImpl( + db: db, + log: logger ?? Logger('AttachmentQueue'), + maxArchivedCount: archivedCacheLimit, + attachmentsQueueTableName: attachmentsQueueTableName, + ); - @override - Future init() async { - // Initialize resources, DB, etc. + syncingService = SyncingService( + remoteStorage: remoteStorage, + localStorage: localStorage, + attachmentsService: attachmentsService, + getLocalUri: (filename) async => getLocalUri(filename), + errorHandler: errorHandler, + syncThrottle: syncThrottleDuration, + period: syncInterval, + ); + logger?.info('syncingService: $syncingService'); } - @override + /// Initialize the attachment queue by: + /// 1. Creating the attachments directory. + /// 2. Adding watches for uploads, downloads, and deletes. + /// 3. Adding a trigger to run uploads, downloads, and deletes when the device is online after being offline. + Future startSync() async { + await _mutex.lock(() async { + if (_closed) { + throw Exception('Attachment queue has been closed'); + } + + await _stopSyncingInternal(); + + logger.info('startSync attachmentsDirectory: $attachmentsDirectory'); + await localStorage.makeDir(attachmentsDirectory); + + subdirectories?.forEach((subdirectory) async { + await localStorage.makeDir('$attachmentsDirectory/$subdirectory'); + }); + + await attachmentsService.withContext((context) async { + await _verifyAttachments(context); + }); + + await syncingService.startSync(); + + // Listen for connectivity changes and watched attachments + _syncStatusSubscription = watchAttachments().listen((items) async { + await _processWatchedAttachments(items); + }); + + logger.info('AttachmentQueue started syncing.'); + }); + } + + /// Stops syncing. Syncing may be resumed with [startSync]. + Future stopSyncing() async { + await _mutex.lock(() async { + await _stopSyncingInternal(); + }); + } + + Future _stopSyncingInternal() async { + if (_closed) return; + + await _syncStatusSubscription?.cancel(); + _syncStatusSubscription = null; + await syncingService.stopSync(); + + logger.info('AttachmentQueue stopped syncing.'); + } + + /// Closes the queue. The queue cannot be used after closing. Future close() async { - await _activeAttachmentsController.close(); - // Clean up resources + await _mutex.lock(() async { + if (_closed) return; + + await _syncStatusSubscription?.cancel(); + await syncingService.close(); + _closed = true; + + logger.info('AttachmentQueue closed.'); + }); } - @override - Stream> watchActiveAttachments() => _activeAttachmentsController.stream; + /// Resolves the filename for new attachment items. + /// Concatenates the attachment ID and extension by default. + Future resolveNewAttachmentFilename( + String attachmentId, + String? fileExtension, + ) async { + return fileExtension != null + ? '$attachmentId.$fileExtension' + : '$attachmentId.dat'; + } + + /// Processes attachment items returned from [watchAttachments]. + /// The default implementation asserts the items returned from [watchAttachments] as the definitive + /// state for local attachments. + Future _processWatchedAttachments( + List items, + ) async { + await attachmentsService.withContext((context) async { + final currentAttachments = await context.getAttachments(); + final List attachmentUpdates = []; + + for (final item in items) { + final existingQueueItem = currentAttachments + .cast() + .firstWhere( + (a) => a != null && a.id == item.id, + orElse: () => null, + ); + + if (existingQueueItem == null) { + if (!downloadAttachments) continue; + + // This item should be added to the queue. + // This item is assumed to be coming from an upstream sync. + final String filename = + item.filename ?? + await resolveNewAttachmentFilename(item.id, item.fileExtension); + + attachmentUpdates.add( + Attachment( + id: item.id, + filename: filename, + state: AttachmentState.queuedDownload, + metaData: item.metaData, + ), + ); + } else if (existingQueueItem.state == AttachmentState.archived) { + // The attachment is present again. Need to queue it for sync. + if (existingQueueItem.hasSynced) { + // No remote action required, we can restore the record (avoids deletion). + attachmentUpdates.add( + existingQueueItem.copyWith(state: AttachmentState.synced), + ); + } else { + // The localURI should be set if the record was meant to be downloaded + // and has been synced. If it's missing and hasSynced is false then + // it must be an upload operation. + attachmentUpdates.add( + existingQueueItem.copyWith( + state: existingQueueItem.localUri == null + ? AttachmentState.queuedDownload + : AttachmentState.queuedUpload, + ), + ); + } + } + } + + // Archive any items not specified in the watched items. + // For QUEUED_DELETE or QUEUED_UPLOAD states, archive only if hasSynced is true. + // For other states, archive if the record is not found in the items. + for (final attachment in currentAttachments) { + final notInWatchedItems = items.every( + (update) => update.id != attachment.id, + ); - @override - Future> getActiveAttachments({AttachmentState? state}) async { - return _attachments.where((a) => - a.state != AttachmentState.archived && - (state == null || a.state == state) - ).toList(); + if (notInWatchedItems) { + switch (attachment.state) { + case AttachmentState.queuedDelete: + case AttachmentState.queuedUpload: + if (attachment.hasSynced) { + attachmentUpdates.add( + attachment.copyWith(state: AttachmentState.archived), + ); + } + break; + default: + attachmentUpdates.add( + attachment.copyWith(state: AttachmentState.archived), + ); + } + } + } + + await context.saveAttachments(attachmentUpdates); + }); } - @override - Future triggerSync() async { - // Implement sync logic, update states, handle errors + /// Creates a new attachment locally and queues it for upload. + /// The filename is resolved using [resolveNewAttachmentFilename]. + Future saveFile({ + required Stream data, + required String mediaType, + String? fileExtension, + String? metaData, + required Future Function(dynamic context, Attachment attachment) + updateHook, + }) async { + final row = await db.get('SELECT uuid() as id'); + final id = row['id'] as String; + final String filename = await resolveNewAttachmentFilename( + id, + fileExtension, + ); + final String localUri = getLocalUri(filename); + + // Write the file to the filesystem. + final fileSize = await localStorage.saveFile(localUri, data); + + return await attachmentsService.withContext((attachmentContext) async { + return await db.writeTransaction((tx) async { + final attachment = Attachment( + id: id, + filename: filename, + size: fileSize, + mediaType: mediaType, + state: AttachmentState.queuedUpload, + localUri: localUri, + metaData: metaData, + ); + + // Allow consumers to set relationships to this attachment ID. + await updateHook(tx, attachment); + + return await attachmentContext.upsertAttachment(attachment, tx); + }); + }); + } + + /// Queues an attachment for delete. + /// The default implementation assumes the attachment record already exists locally. + Future deleteFile({ + required String attachmentId, + required Future Function(dynamic context, Attachment attachment) + updateHook, + }) async { + return await attachmentsService.withContext((attachmentContext) async { + final attachment = await attachmentContext.getAttachment(attachmentId); + if (attachment == null) { + throw Exception( + 'Attachment record with id $attachmentId was not found.', + ); + } + + return await db.writeTransaction((tx) async { + await updateHook(tx, attachment); + return await attachmentContext.upsertAttachment( + attachment.copyWith( + state: AttachmentState.queuedDelete, + hasSynced: false, + ), + tx, + ); + }); + }); + } + + /// Returns the user's storage directory with the attachment path used to load the file. + String getLocalUri(String filename) { + return '$attachmentsDirectory/$filename'; } - @override - Future> getAttachments({int? limit, int? offset}) async { - var list = List.from(_attachments); - if (offset != null) list = list.skip(offset).toList(); - if (limit != null) list = list.take(limit).toList(); - return list; + /// Removes all archived items. + Future expireCache() async { + await attachmentsService.withContext((context) async { + bool done; + do { + done = await syncingService.deleteArchivedAttachments(context); + } while (!done); + }); } - @override - Future withContext(Future Function(AttachmentContext ctx) action) async { - await _mutex.protect(() async { - final ctx = AttachmentContext(); - await action(ctx); + /// Clears the attachment queue and deletes all attachment files. + Future clearQueue() async { + await attachmentsService.withContext((context) async { + await context.clearQueue(); }); + await localStorage.rmDir(attachmentsDirectory); } - // Add methods for adding, updating, removing attachments, etc. -} \ No newline at end of file + /// Cleans up stale attachments. + Future _verifyAttachments(AttachmentContext context) async { + final attachments = await context.getActiveAttachments(); + final List updates = []; + + for (final attachment in attachments) { + // Only check attachments that should have local files + if (attachment.localUri == null) { + // Skip attachments that don't have localUri (like queued downloads) + continue; + } + + final exists = await localStorage.fileExists(attachment.localUri!); + if ((attachment.state == AttachmentState.synced || + attachment.state == AttachmentState.queuedUpload) && + !exists) { + updates.add( + attachment.copyWith(state: AttachmentState.archived, localUri: null), + ); + } + } + + await context.saveAttachments(updates); + } +} diff --git a/packages/powersync_attachments_stream/lib/src/attachment_service.dart b/packages/powersync_attachments_stream/lib/src/attachment_service.dart deleted file mode 100644 index 9c85af8c..00000000 --- a/packages/powersync_attachments_stream/lib/src/attachment_service.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'attachment_state.dart'; -import 'attachment_context.dart'; - -abstract class AttachmentService { - Future init(); - Future close(); - - Stream> watchActiveAttachments(); - Future> getActiveAttachments({AttachmentState? state}); - Future triggerSync(); - Future> getAttachments({int? limit, int? offset}); - Future withContext(Future Function(AttachmentContext ctx) action); -} \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/src/attachment_state.dart b/packages/powersync_attachments_stream/lib/src/attachment_state.dart deleted file mode 100644 index f90cffda..00000000 --- a/packages/powersync_attachments_stream/lib/src/attachment_state.dart +++ /dev/null @@ -1,47 +0,0 @@ -enum AttachmentState { - pending, - uploading, - uploaded, - failed, - archived, - synced, // New state -} - -class Attachment { - final String id; - final AttachmentState state; - final bool hasSynced; - final Map? metaData; - final String? mediaType; - final DateTime? createdAt; - final DateTime? updatedAt; - - Attachment({ - required this.id, - required this.state, - this.hasSynced = false, - this.metaData, - this.mediaType, - this.createdAt, - this.updatedAt, - }); - - Attachment copyWith({ - AttachmentState? state, - bool? hasSynced, - Map? metaData, - String? mediaType, - DateTime? createdAt, - DateTime? updatedAt, - }) { - return Attachment( - id: id, - state: state ?? this.state, - hasSynced: hasSynced ?? this.hasSynced, - metaData: metaData ?? this.metaData, - mediaType: mediaType ?? this.mediaType, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - ); - } -} \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart b/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart new file mode 100644 index 00000000..4eee509e --- /dev/null +++ b/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart @@ -0,0 +1,191 @@ +import '../abstractions/attachment_context.dart'; +import '../attachment.dart'; +import 'package:powersync_core/powersync_core.dart'; +import 'package:powersync_core/sqlite3_common.dart'; +import 'package:logging/logging.dart'; + +class AttachmentContextImpl implements AttachmentContext { + final PowerSyncDatabase db; + final Logger log; + final int maxArchivedCount; + final String attachmentsQueueTableName; + + AttachmentContextImpl( + this.db, + this.log, + this.maxArchivedCount, + this.attachmentsQueueTableName, + ); + + /// Table used for storing attachments in the attachment queue. + String get table { + return attachmentsQueueTableName; + } + + @override + Future deleteAttachment(String id, dynamic tx) async { + log.info('deleteAttachment: $id'); + await tx.execute('DELETE FROM $table WHERE id = ?', [id]); + } + + @override + Future ignoreAttachment(String id) async { + await db.execute( + 'UPDATE $table SET state = ${AttachmentState.archived.index} WHERE id = ?', + [id], + ); + } + + @override + Future getAttachment(String id) async { + final row = await db.getOptional('SELECT * FROM $table WHERE id = ?', [id]); + if (row == null) { + return null; + } + return Attachment.fromRow(row); + } + + @override + Future saveAttachment(Attachment attachment) async { + Attachment updatedRecord = attachment.copyWith( + timestamp: DateTime.now().millisecondsSinceEpoch, + ); + + await db.execute( + ''' + INSERT OR REPLACE INTO $table + (id, timestamp, filename, local_uri, media_type, size, state) VALUES (?, ?, ?, ?, ?, ?, ?) + ''', + [ + updatedRecord.id, + updatedRecord.timestamp, + updatedRecord.filename, + updatedRecord.localUri, + updatedRecord.mediaType, + updatedRecord.size, + updatedRecord.state.index, + ], + ); + + return updatedRecord; + } + + @override + Future saveAttachments(List attachments) async { + if (attachments.isEmpty) { + log.info('No attachments to save.'); + return; + } + + final updatedRecords = attachments.map((attachment) { + final updated = attachment.copyWith( + timestamp: DateTime.now().millisecondsSinceEpoch, + ); + return [ + updated.id, + updated.filename, + updated.localUri, + updated.mediaType, + updated.size, + updated.timestamp, + updated.state.index, + ]; + }).toList(); + + log.info('Saving ${updatedRecords.length} attachments...'); + + await db.executeBatch(''' + INSERT OR REPLACE INTO $table + (id, filename, local_uri, media_type, size, timestamp, state) VALUES (?, ?, ?, ?, ?, ?, ?) + ''', updatedRecords); + } + + @override + Future> getAttachmentIds() async { + ResultSet results = await db.getAll( + 'SELECT id FROM $table WHERE id IS NOT NULL', + ); + + List ids = results.map((row) => row['id'] as String).toList(); + + return ids; + } + + @override + Future> getAttachments() async { + final results = await db.getAll('SELECT * FROM $table'); + return results.map((row) => Attachment.fromRow(row)).toList(); + } + + @override + Future> getActiveAttachments() async { + // Return all attachments that are not archived (i.e., state != AttachmentState.archived) + final results = await db.getAll('SELECT * FROM $table WHERE state != ?', [ + AttachmentState.archived.index, + ]); + return results.map((row) => Attachment.fromRow(row)).toList(); + } + + @override + Future clearQueue() async { + log.info('Clearing attachment queue...'); + await db.execute('DELETE FROM $table'); + } + + @override + Future deleteArchivedAttachments( + Future Function(List) callback, + ) async { + // Find all archived attachments + final results = await db.getAll('SELECT * FROM $table WHERE state = ?', [ + AttachmentState.archived.index, + ]); + final archivedAttachments = results + .map((row) => Attachment.fromRow(row)) + .toList(); + + if (archivedAttachments.isEmpty) { + return false; + } + + log.info('Deleting ${archivedAttachments.length} archived attachments...'); + // Call the callback with the list of archived attachments before deletion + await callback(archivedAttachments); + + // Delete the archived attachments from the table + final ids = archivedAttachments.map((a) => a.id).toList(); + // Use a batch delete for efficiency + final placeholders = List.filled(ids.length, '?').join(','); + await db.execute('DELETE FROM $table WHERE id IN ($placeholders)', ids); + + log.info('Deleted ${archivedAttachments.length} archived attachments.'); + return true; + } + + @override + Future upsertAttachment( + Attachment attachment, + dynamic context, + ) async { + + await context.execute( + '''INSERT OR REPLACE INTO + $table (id, timestamp, filename, local_uri, media_type, size, state, has_synced, meta_data) + VALUES + (?, ?, ?, ?, ?, ?, ?, ?, ?)''', + [ + attachment.id, + attachment.timestamp, + attachment.filename, + attachment.localUri, + attachment.mediaType, + attachment.size, + attachment.state.index, + // attachment.state, + attachment.hasSynced ? 1 : 0, + attachment.metaData, + ], + ); + return attachment; + } +} diff --git a/packages/powersync_attachments_stream/lib/src/implementations/attachment_service.dart b/packages/powersync_attachments_stream/lib/src/implementations/attachment_service.dart new file mode 100644 index 00000000..feefa115 --- /dev/null +++ b/packages/powersync_attachments_stream/lib/src/implementations/attachment_service.dart @@ -0,0 +1,63 @@ +import 'dart:async'; +import 'package:logging/logging.dart'; +import 'package:powersync_core/powersync_core.dart'; + +import '../abstractions/attachment_service.dart'; +import '../abstractions/attachment_context.dart'; +import '../attachment.dart'; +import 'attachment_context.dart'; + +class AttachmentServiceImpl implements AttachmentService { + final PowerSyncDatabase db; + final Logger log; + final int maxArchivedCount; + final String attachmentsQueueTableName; + Future _mutex = Future.value(); + + late final AttachmentContext _context; + + AttachmentServiceImpl({ + required this.db, + required this.log, + required this.maxArchivedCount, + required this.attachmentsQueueTableName, + }) { + _context = AttachmentContextImpl(db, log, maxArchivedCount, attachmentsQueueTableName); + } + + @override + Stream watchActiveAttachments() async* { + log.info('Watching attachments...'); + + // Watch for attachments with active states (queued for upload, download, or delete) + final stream = db.watch( + ''' + SELECT + id + FROM + $attachmentsQueueTableName + WHERE + state = ? + OR state = ? + OR state = ? + ORDER BY + timestamp ASC + ''', + parameters: [ + AttachmentState.queuedUpload.index, + AttachmentState.queuedDownload.index, + AttachmentState.queuedDelete.index, + ], + ); + + yield* stream; + } + + @override + Future withContext(Future Function(AttachmentContext ctx) action) { + // Simple mutex using chained futures + final completer = Completer(); + _mutex = _mutex.then((_) => action(_context)).then(completer.complete).catchError(completer.completeError); + return completer.future; + } +} \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/src/local_storage.dart b/packages/powersync_attachments_stream/lib/src/local_storage.dart deleted file mode 100644 index 918b3ed4..00000000 --- a/packages/powersync_attachments_stream/lib/src/local_storage.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'package:path/path.dart' as p; - -abstract class LocalStorage { - /// Saves binary data to storage with an identifier - /// - /// [id] - Unique identifier for the stored data - /// [bytes] - Binary data to store - /// [mediaType] - Optional MIME type of the data (e.g., 'image/jpeg') - /// [metadata] - Optional key-value pairs for additional data information - Future save(String id, List bytes, {String? mediaType, Map? metadata}); - - /// Retrieves binary data by identifier - /// - /// [id] - Unique identifier of the data to retrieve - /// Returns the binary data if found, null otherwise - Future?> read(String id); - - /// Removes data and its metadata from storage - /// - /// [id] - Unique identifier of the data to delete - Future delete(String id); - - /// Retrieves metadata associated with stored data - /// - /// [id] - Unique identifier of the data - /// Returns metadata map if found, null otherwise - Future?> getMetadata(String id); -} - -class IOLocalStorage implements LocalStorage { - final Directory baseDir; - - IOLocalStorage(this.baseDir); - - File _fileFor(String id) => File(p.join(baseDir.path, id)); - File _metaFileFor(String id) => File(p.join(baseDir.path, '$id.meta.json')); - - @override - Future save(String id, List bytes, {String? mediaType, Map? metadata}) async { - await baseDir.create(recursive: true); - await _fileFor(id).writeAsBytes(bytes); - if (mediaType != null || metadata != null) { - final meta = {}; - if (mediaType != null) meta['mediaType'] = mediaType; - if (metadata != null) meta['metadata'] = metadata; - await _metaFileFor(id).writeAsString(jsonEncode(meta)); - } - } - - @override - Future?> read(String id) async { - final file = _fileFor(id); - if (await file.exists()) { - return await file.readAsBytes(); - } - return null; - } - - @override - Future delete(String id) async { - try { - await _fileFor(id).delete(); - } on FileSystemException { - // File doesn't exist, ignore - } - try { - await _metaFileFor(id).delete(); - } on FileSystemException { - // File doesn't exist, ignore - } - } - - @override - Future?> getMetadata(String id) async { - final metaFile = _metaFileFor(id); - if (await metaFile.exists()) { - final content = await metaFile.readAsString(); - return jsonDecode(content) as Map; - } - return null; - } -} \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart b/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart new file mode 100644 index 00000000..ea28ef50 --- /dev/null +++ b/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart @@ -0,0 +1,98 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:path/path.dart' as p; +import '../abstractions/local_storage.dart'; + +class IOLocalStorage implements LocalStorage { + final Directory baseDir; + + IOLocalStorage(this.baseDir); + + File _fileFor(String filePath) => File(p.join(baseDir.path, filePath)); + File _metaFileFor(String filePath) => + File(p.join(baseDir.path, ' [200m$filePath.meta.json [201m')); + + @override + Future saveFile(String filePath, Stream data) async { + final file = _fileFor(filePath); + await file.parent.create(recursive: true); + var totalSize = 0; + final sink = file.openWrite(); + try { + await for (final chunk in data) { + sink.add(chunk); + totalSize += chunk.length; + } + await sink.flush(); + } finally { + await sink.close(); + } + return totalSize; + } + + @override + Stream readFile(String filePath, {String? mediaType}) async* { + final file = _fileFor(filePath); + if (!await file.exists()) { + throw FileSystemException('File does not exist', filePath); + } + final source = file.openRead(); + await for (final chunk in source) { + yield chunk is Uint8List ? chunk : Uint8List.fromList(chunk); + } + } + + @override + Future deleteFile(String filePath) async { + final file = _fileFor(filePath); + if (await file.exists()) { + await file.delete(); + } + final metaFile = _metaFileFor(filePath); + if (await metaFile.exists()) { + await metaFile.delete(); + } + } + + @override + Future fileExists(String filePath) async { + return await _fileFor(filePath).exists(); + } + + @override + Future makeDir(String path) async { + await Directory(p.join(baseDir.path, path)).create(recursive: true); + } + + @override + Future rmDir(String path) async { + final dir = Directory(p.join(baseDir.path, path)); + if (await dir.exists()) { + await for (final entity in dir.list(recursive: false)) { + if (entity is Directory) { + await rmDir(p.relative(entity.path, from: baseDir.path)); + } else if (entity is File) { + await entity.delete(); + } + } + await dir.delete(); + } + } + + @override + Future copyFile(String sourcePath, String targetPath) async { + final sourceFile = _fileFor(sourcePath); + final targetFile = _fileFor(targetPath); + if (!await sourceFile.exists()) { + throw FileSystemException('Source file does not exist', sourcePath); + } + await targetFile.parent.create(recursive: true); + await sourceFile.copy(targetFile.path); + final sourceMeta = _metaFileFor(sourcePath); + final targetMeta = _metaFileFor(targetPath); + if (await sourceMeta.exists()) { + await sourceMeta.copy(targetMeta.path); + } + } +} diff --git a/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart b/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart new file mode 100644 index 00000000..bfb207c8 --- /dev/null +++ b/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart @@ -0,0 +1,283 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'package:logging/logging.dart'; +import 'package:async/async.dart'; + +import '../abstractions/attachment_service.dart'; +import '../abstractions/attachment_context.dart'; +import '../attachment.dart'; +import '../abstractions/local_storage.dart'; +import '../abstractions/remote_storage.dart'; +import '../sync_error_handler.dart'; + +class SyncingService { + final RemoteStorage remoteStorage; + final LocalStorage localStorage; + final AttachmentService attachmentsService; + final Future Function(String) getLocalUri; + final SyncErrorHandler? errorHandler; + final Duration syncThrottle; + final Duration period; + final _log = Logger('SyncingService'); + + StreamSubscription? _syncSubscription; + StreamSubscription? _periodicSubscription; + bool _isClosed = false; + final _syncTriggerController = StreamController.broadcast(); + + SyncingService({ + required this.remoteStorage, + required this.localStorage, + required this.attachmentsService, + required this.getLocalUri, + this.errorHandler, + this.syncThrottle = const Duration(seconds: 5), + this.period = const Duration(seconds: 30), + }); + + Future startSync() async { + if (_isClosed) return; + + _syncSubscription?.cancel(); + _periodicSubscription?.cancel(); + + // Create a merged stream of manual triggers and attachment changes + final attachmentChanges = attachmentsService.watchActiveAttachments(); + final manualTriggers = _syncTriggerController.stream; + + // Merge both streams and apply throttling + final mergedStream = StreamGroup.merge([attachmentChanges, manualTriggers]) + .transform(_throttleTransformer(syncThrottle)) + .listen((_) async { + try { + await attachmentsService.withContext((context) async { + final attachments = await context.getActiveAttachments(); + _log.info( + 'active attachments: ${attachments.map((e) => e.id).toList()}', + ); + _log.info( + 'SyncingService: Found ${attachments.length} active attachments', + ); + await handleSync(attachments, context); + await deleteArchivedAttachments(context); + }); + } catch (e, st) { + if (e is! StateError && e.toString().contains('cancelled')) { + _log.severe( + 'Caught exception when processing attachments', + e, + st, + ); + } else { + rethrow; + } + } + }); + + _syncSubscription = mergedStream; + + // Start periodic sync + _periodicSubscription = Stream.periodic(period).listen((_) { + _log.info('Periodically syncing attachments'); + triggerSync(); + }); + } + + StreamTransformer _throttleTransformer(Duration throttle) { + return StreamTransformer.fromHandlers( + handleData: (data, sink) { + sink.add(data); + // Simple throttle implementation - just delay the next event + Future.delayed(throttle); + }, + ); + } + + Future triggerSync() async { + if (_isClosed) return; + _syncTriggerController.add(null); + } + + Future stopSync() async { + await _syncSubscription?.cancel(); + await _periodicSubscription?.cancel(); + } + + Future close() async { + _isClosed = true; + await stopSync(); + await _syncTriggerController.close(); + } + + Future handleSync( + List attachments, + AttachmentContext context, + ) async { + _log.info( + 'SyncingService: Starting handleSync with ${attachments.length} attachments', + ); + final updatedAttachments = []; + + for (final attachment in attachments) { + _log.info( + 'SyncingService: Processing attachment ${attachment.id} with state: ${attachment.state}', + ); + try { + switch (attachment.state) { + case AttachmentState.queuedDownload: + _log.info('SyncingService: Downloading [${attachment.filename}]'); + updatedAttachments.add(await downloadAttachment(attachment)); + break; + case AttachmentState.queuedUpload: + _log.info('SyncingService: Uploading [${attachment.filename}]'); + updatedAttachments.add(await uploadAttachment(attachment)); + break; + case AttachmentState.queuedDelete: + _log.info('SyncingService: Deleting [${attachment.filename}]'); + updatedAttachments.add(await deleteAttachment(attachment)); + break; + case AttachmentState.synced: + _log.info( + 'SyncingService: Attachment ${attachment.id} is already synced', + ); + break; + case AttachmentState.archived: + _log.info( + 'SyncingService: Attachment ${attachment.id} is archived', + ); + break; + } + } catch (e, st) { + _log.warning( + 'SyncingService: Error during sync for ${attachment.id}', + e, + st, + ); + } + } + + if (updatedAttachments.isNotEmpty) { + _log.info( + 'SyncingService: Saving ${updatedAttachments.length} updated attachments', + ); + await context.saveAttachments(updatedAttachments); + } + } + + Future uploadAttachment(Attachment attachment) async { + _log.info( + 'SyncingService: Starting upload for attachment ${attachment.id}', + ); + try { + if (attachment.localUri == null) { + throw Exception('No localUri for attachment $attachment'); + } + _log.info( + 'SyncingService: Calling remoteStorage.uploadFile for ${attachment.id}', + ); + await remoteStorage.uploadFile( + localStorage.readFile(attachment.localUri!), + attachment, + ); + _log.info( + 'SyncingService: Successfully uploaded attachment "${attachment.id}" to Cloud Storage', + ); + return attachment.copyWith( + state: AttachmentState.synced, + hasSynced: true, + ); + } catch (e, st) { + _log.warning( + 'SyncingService: Upload attachment error for attachment $attachment', + e, + st, + ); + if (errorHandler != null) { + final shouldRetry = await errorHandler!.onUploadError(attachment, e); + if (!shouldRetry) { + _log.info( + 'SyncingService: Attachment with ID ${attachment.id} has been archived', + ); + return attachment.copyWith(state: AttachmentState.archived); + } + } + return attachment; + } + } + + Future downloadAttachment(Attachment attachment) async { + _log.info( + 'SyncingService: Starting download for attachment ${attachment.id}', + ); + final attachmentPath = await getLocalUri(attachment.filename); + try { + _log.info( + 'SyncingService: Calling remoteStorage.downloadFile for ${attachment.id}', + ); + final fileStream = await remoteStorage.downloadFile(attachment); + await localStorage.saveFile( + attachmentPath, + fileStream.map((chunk) => Uint8List.fromList(chunk)), + ); + _log.info( + 'SyncingService: Successfully downloaded file "${attachment.id}"', + ); + return attachment.copyWith( + localUri: attachmentPath, + state: AttachmentState.synced, + hasSynced: true, + ); + } catch (e, st) { + if (errorHandler != null) { + final shouldRetry = await errorHandler!.onDownloadError(attachment, e); + if (!shouldRetry) { + _log.info( + 'SyncingService: Attachment with ID ${attachment.id} has been archived', + ); + return attachment.copyWith(state: AttachmentState.archived); + } + } + _log.warning( + 'SyncingService: Download attachment error for attachment $attachment', + e, + st, + ); + return attachment; + } + } + + Future deleteAttachment(Attachment attachment) async { + try { + _log.info( + 'SyncingService: Deleting attachment ${attachment.id} from remote storage', + ); + await remoteStorage.deleteFile(attachment); + + if (attachment.localUri != null && + await localStorage.fileExists(attachment.localUri!)) { + await localStorage.deleteFile(attachment.localUri!); + } + return attachment.copyWith(state: AttachmentState.archived); + } catch (e, st) { + if (errorHandler != null) { + final shouldRetry = await errorHandler!.onDeleteError(attachment, e); + if (!shouldRetry) { + _log.info('Attachment with ID ${attachment.id} has been archived'); + return attachment.copyWith(state: AttachmentState.archived); + } + } + _log.warning('Error deleting attachment: $e', e, st); + return attachment; + } + } + + Future deleteArchivedAttachments(AttachmentContext context) async { + return context.deleteArchivedAttachments((pendingDelete) async { + for (final attachment in pendingDelete) { + if (attachment.localUri == null) continue; + if (!await localStorage.fileExists(attachment.localUri!)) continue; + await localStorage.deleteFile(attachment.localUri!); + } + }); + } +} diff --git a/packages/powersync_attachments_stream/lib/src/sync_error_handler.dart b/packages/powersync_attachments_stream/lib/src/sync_error_handler.dart index 89ffe00b..c6a5795b 100644 --- a/packages/powersync_attachments_stream/lib/src/sync_error_handler.dart +++ b/packages/powersync_attachments_stream/lib/src/sync_error_handler.dart @@ -1,6 +1,36 @@ -import 'attachment_state.dart'; +import './attachment.dart'; +/// Interface for handling errors during attachment operations. +/// Implementations determine whether failed operations should be retried. +/// Attachment records are archived if an operation fails and should not be retried. abstract class SyncErrorHandler { - /// Return true to retry, false to skip/fail - Future onError(Object error, StackTrace stack, {Attachment? attachment}); + /// Determines whether the provided attachment download operation should be retried. + /// + /// [attachment] The attachment involved in the failed download operation. + /// [exception] The exception that caused the download failure. + /// Returns `true` if the download operation should be retried, `false` otherwise. + Future onDownloadError( + Attachment attachment, + Object exception, + ); + + /// Determines whether the provided attachment upload operation should be retried. + /// + /// [attachment] The attachment involved in the failed upload operation. + /// [exception] The exception that caused the upload failure. + /// Returns `true` if the upload operation should be retried, `false` otherwise. + Future onUploadError( + Attachment attachment, + Object exception, + ); + + /// Determines whether the provided attachment delete operation should be retried. + /// + /// [attachment] The attachment involved in the failed delete operation. + /// [exception] The exception that caused the delete failure. + /// Returns `true` if the delete operation should be retried, `false` otherwise. + Future onDeleteError( + Attachment attachment, + Object exception, + ); } \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/src/utils/mutex.dart b/packages/powersync_attachments_stream/lib/src/utils/mutex.dart deleted file mode 100644 index d62c919e..00000000 --- a/packages/powersync_attachments_stream/lib/src/utils/mutex.dart +++ /dev/null @@ -1,33 +0,0 @@ -// lib/src/utils/mutex.dart - -import 'dart:async'; - -class AsyncMutex { - bool _locked = false; - final List _queue = []; - - Future protect(Future Function() action) async { - if (_locked) { - final completer = Completer(); - _queue.add(() async { - try { - completer.complete(await action()); - } catch (e, st) { - completer.completeError(e, st); - } - }); - return completer.future; - } else { - _locked = true; - try { - return await action(); - } finally { - _locked = false; - if (_queue.isNotEmpty) { - final next = _queue.removeAt(0); - next(); - } - } - } - } -} \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/src/utils/streams.dart b/packages/powersync_attachments_stream/lib/src/utils/streams.dart deleted file mode 100644 index 126192f1..00000000 --- a/packages/powersync_attachments_stream/lib/src/utils/streams.dart +++ /dev/null @@ -1 +0,0 @@ -// Utilities for stream management, e.g., debouncing, distinct, etc. \ No newline at end of file diff --git a/packages/powersync_attachments_stream/pubspec.yaml b/packages/powersync_attachments_stream/pubspec.yaml index c5e00e5f..42e6f761 100644 --- a/packages/powersync_attachments_stream/pubspec.yaml +++ b/packages/powersync_attachments_stream/pubspec.yaml @@ -6,12 +6,12 @@ homepage: https://www.powersync.com/ environment: sdk: ^3.8.1 - flutter: ">=1.17.0" dependencies: flutter: sdk: flutter path: ^1.8.0 + async: ^2.11.0 powersync_core: ^1.4.1 logging: ^1.2.0 @@ -23,10 +23,3 @@ dev_dependencies: sdk: flutter flutter_lints: ^5.0.0 test: ^1.25.15 - -platforms: - android: - ios: - linux: - macos: - windows: diff --git a/packages/powersync_attachments_stream/test/local_storage.dart b/packages/powersync_attachments_stream/test/local_storage.dart deleted file mode 100644 index 20a42748..00000000 --- a/packages/powersync_attachments_stream/test/local_storage.dart +++ /dev/null @@ -1,274 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'package:test/test.dart'; -import 'package:path/path.dart' as p; -import 'package:powersync_attachments_stream/src/local_storage.dart'; - -void main() { - group('IOLocalStorage', () { - late Directory tempDir; - late IOLocalStorage storage; - - setUp(() async { - tempDir = await Directory.systemTemp.createTemp('local_storage_test_'); - storage = IOLocalStorage(tempDir); - }); - - tearDown(() async { - if (await tempDir.exists()) { - await tempDir.delete(recursive: true); - } - }); - - group('save and read', () { - test('saves and reads binary data successfully', () async { - const id = 'test_file'; - final data = [1, 2, 3, 4, 5]; - - await storage.save(id, data); - final result = await storage.read(id); - - expect(result, equals(data)); - }); - - test('returns null when reading non-existent file', () async { - final result = await storage.read('non_existent'); - expect(result, isNull); - }); - - test('creates base directory if it does not exist', () async { - final nonExistentDir = Directory(p.join(tempDir.path, 'subdir', 'nested')); - final nestedStorage = IOLocalStorage(nonExistentDir); - - expect(await nonExistentDir.exists(), isFalse); - - await nestedStorage.save('test', [1, 2, 3]); - - expect(await nonExistentDir.exists(), isTrue); - final result = await nestedStorage.read('test'); - expect(result, equals([1, 2, 3])); - }); - - test('overwrites existing file', () async { - const id = 'overwrite_test'; - final originalData = [1, 2, 3]; - final newData = [4, 5, 6, 7]; - - await storage.save(id, originalData); - await storage.save(id, newData); - - final result = await storage.read(id); - expect(result, equals(newData)); - }); - }); - - group('metadata', () { - test('saves and retrieves metadata with mediaType only', () async { - const id = 'test_with_media_type'; - final data = [1, 2, 3]; - const mediaType = 'image/jpeg'; - - await storage.save(id, data, mediaType: mediaType); - final metadata = await storage.getMetadata(id); - - expect(metadata, isNotNull); - expect(metadata!['mediaType'], equals(mediaType)); - expect(metadata['metadata'], isNull); - }); - - test('saves and retrieves metadata with custom metadata only', () async { - const id = 'test_with_metadata'; - final data = [1, 2, 3]; - final customMetadata = {'width': 800, 'height': 600, 'format': 'png'}; - - await storage.save(id, data, metadata: customMetadata); - final metadata = await storage.getMetadata(id); - - expect(metadata, isNotNull); - expect(metadata!['mediaType'], isNull); - expect(metadata['metadata'], equals(customMetadata)); - }); - - test('saves and retrieves both mediaType and custom metadata', () async { - const id = 'test_with_both'; - final data = [1, 2, 3]; - const mediaType = 'application/pdf'; - final customMetadata = {'pages': 10, 'author': 'Test Author'}; - - await storage.save(id, data, mediaType: mediaType, metadata: customMetadata); - final metadata = await storage.getMetadata(id); - - expect(metadata, isNotNull); - expect(metadata!['mediaType'], equals(mediaType)); - expect(metadata['metadata'], equals(customMetadata)); - }); - - test('returns null when getting metadata for non-existent file', () async { - final metadata = await storage.getMetadata('non_existent'); - expect(metadata, isNull); - }); - - test('does not create metadata file when no metadata provided', () async { - const id = 'no_metadata'; - final data = [1, 2, 3]; - - await storage.save(id, data); - - final metaFile = File(p.join(tempDir.path, '$id.meta.json')); - expect(await metaFile.exists(), isFalse); - - final metadata = await storage.getMetadata(id); - expect(metadata, isNull); - }); - - test('overwrites existing metadata', () async { - const id = 'metadata_overwrite'; - final data = [1, 2, 3]; - final originalMetadata = {'version': 1}; - final newMetadata = {'version': 2, 'updated': true}; - - await storage.save(id, data, metadata: originalMetadata); - await storage.save(id, data, metadata: newMetadata); - - final metadata = await storage.getMetadata(id); - expect(metadata!['metadata'], equals(newMetadata)); - }); - }); - - group('delete', () { - test('deletes existing file and metadata', () async { - const id = 'delete_test'; - final data = [1, 2, 3]; - final customMetadata = {'test': true}; - - await storage.save(id, data, mediaType: 'text/plain', metadata: customMetadata); - - // Verify files exist - expect(await storage.read(id), isNotNull); - expect(await storage.getMetadata(id), isNotNull); - - await storage.delete(id); - - // Verify files are deleted - expect(await storage.read(id), isNull); - expect(await storage.getMetadata(id), isNull); - }); - - test('does not throw when deleting non-existent files', () async { - // Should not throw exception - await storage.delete('non_existent'); - }); - - test('deletes only data file when metadata does not exist', () async { - const id = 'data_only'; - final data = [1, 2, 3]; - - await storage.save(id, data); // No metadata - expect(await storage.read(id), isNotNull); - - await storage.delete(id); - expect(await storage.read(id), isNull); - }); - - test('handles partial deletion gracefully', () async { - const id = 'partial_delete'; - final data = [1, 2, 3]; - - await storage.save(id, data, metadata: {'test': true}); - - // Manually delete just the data file - final dataFile = File(p.join(tempDir.path, id)); - await dataFile.delete(); - - // Delete should still work without throwing - await storage.delete(id); - - expect(await storage.read(id), isNull); - expect(await storage.getMetadata(id), isNull); - }); - }); - - group('file system integration', () { - test('handles special characters in id', () async { - const id = 'file with spaces & symbols!@#'; - final data = [1, 2, 3]; - - await storage.save(id, data); - final result = await storage.read(id); - - expect(result, equals(data)); - }); - - test('handles empty data', () async { - const id = 'empty_file'; - final data = []; - - await storage.save(id, data); - final result = await storage.read(id); - - expect(result, equals(data)); - }); - - test('handles large binary data', () async { - const id = 'large_file'; - final data = List.generate(10000, (i) => i % 256); - - await storage.save(id, data); - final result = await storage.read(id); - - expect(result, equals(data)); - }); - - test('metadata file has correct JSON format', () async { - const id = 'json_format_test'; - final data = [1, 2, 3]; - const mediaType = 'application/json'; - final customMetadata = {'test': true, 'number': 42}; - - await storage.save(id, data, mediaType: mediaType, metadata: customMetadata); - - // Read metadata file directly - final metaFile = File(p.join(tempDir.path, '$id.meta.json')); - final content = await metaFile.readAsString(); - final parsed = jsonDecode(content) as Map; - - expect(parsed['mediaType'], equals(mediaType)); - expect(parsed['metadata'], equals(customMetadata)); - }); - }); - - group('concurrent operations', () { - test('handles concurrent saves to different files', () async { - final futures = >[]; - - for (int i = 0; i < 10; i++) { - futures.add(storage.save('file_$i', [i, i + 1, i + 2])); - } - - await Future.wait(futures); - - for (int i = 0; i < 10; i++) { - final result = await storage.read('file_$i'); - expect(result, equals([i, i + 1, i + 2])); - } - }); - - test('handles concurrent operations on same file', () async { - const id = 'concurrent_test'; - final data1 = [1, 2, 3]; - final data2 = [4, 5, 6]; - - final futures = [ - storage.save(id, data1), - storage.save(id, data2), - ]; - - await Future.wait(futures); - - final result = await storage.read(id); - expect(result, isNotNull); - expect(result, anyOf(equals(data1), equals(data2))); - }); - }); - }); -} \ No newline at end of file diff --git a/packages/powersync_attachments_stream/test/local_storage_test.dart b/packages/powersync_attachments_stream/test/local_storage_test.dart new file mode 100644 index 00000000..c90e7121 --- /dev/null +++ b/packages/powersync_attachments_stream/test/local_storage_test.dart @@ -0,0 +1,262 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:test/test.dart'; +import 'package:path/path.dart' as p; +import 'package:powersync_attachments_stream/src/storage/io_local_storage.dart'; + +void main() { + group('IOLocalStorage', () { + late Directory tempDir; + late IOLocalStorage storage; + + setUp(() async { + tempDir = await Directory.systemTemp.createTemp('local_storage_test_'); + storage = IOLocalStorage(tempDir); + }); + + tearDown(() async { + if (await tempDir.exists()) { + await tempDir.delete(recursive: true); + } + }); + + group('saveFile and readFile', () { + test('saves and reads binary data stream successfully', () async { + const filePath = 'test_file'; + final data = Uint8List.fromList([1, 2, 3, 4, 5]); + final dataStream = Stream.fromIterable([data]); + + final size = await storage.saveFile(filePath, dataStream); + expect(size, equals(data.length)); + + final resultStream = storage.readFile(filePath); + final result = await resultStream.toList(); + expect(result, equals([data])); + }); + + test('throws when reading non-existent file', () async { + const filePath = 'non_existent'; + expect( + () => storage.readFile(filePath).toList(), + throwsA(isA()), + ); + }); + + test('creates parent directories if they do not exist', () async { + const filePath = 'subdir/nested/test'; + final nonExistentDir = Directory(p.join(tempDir.path, 'subdir', 'nested')); + final data = Uint8List.fromList([1, 2, 3]); + final dataStream = Stream.fromIterable([data]); + + expect(await nonExistentDir.exists(), isFalse); + + final size = await storage.saveFile(filePath, dataStream); + expect(size, equals(data.length)); + expect(await nonExistentDir.exists(), isTrue); + + final resultStream = storage.readFile(filePath); + final result = await resultStream.toList(); + expect(result, equals([data])); + }); + + test('overwrites existing file', () async { + const filePath = 'overwrite_test'; + final originalData = Uint8List.fromList([1, 2, 3]); + final newData = Uint8List.fromList([4, 5, 6, 7]); + final originalStream = Stream.fromIterable([originalData]); + final newStream = Stream.fromIterable([newData]); + + await storage.saveFile(filePath, originalStream); + final size = await storage.saveFile(filePath, newStream); + expect(size, equals(newData.length)); + + final resultStream = storage.readFile(filePath); + final result = await resultStream.toList(); + expect(result, equals([newData])); + }); + }); + + group('deleteFile', () { + test('deletes existing file', () async { + const filePath = 'delete_test'; + final data = Uint8List.fromList([1, 2, 3]); + final dataStream = Stream.fromIterable([data]); + + await storage.saveFile(filePath, dataStream); + expect(await storage.fileExists(filePath), isTrue); + + await storage.deleteFile(filePath); + expect(await storage.fileExists(filePath), isFalse); + }); + + test('does not throw when deleting non-existent file', () async { + await storage.deleteFile('non_existent'); + }); + }); + + group('fileExists', () { + test('returns true for existing file', () async { + const filePath = 'exists_test'; + final data = Uint8List.fromList([1, 2, 3]); + final dataStream = Stream.fromIterable([data]); + + await storage.saveFile(filePath, dataStream); + expect(await storage.fileExists(filePath), isTrue); + }); + + test('returns false for non-existent file', () async { + expect(await storage.fileExists('non_existent'), isFalse); + }); + }); + + group('makeDir', () { + test('creates directory and its parents', () async { + const dirPath = 'test_dir/subdir'; + final fullPath = Directory(p.join(tempDir.path, dirPath)); + + expect(await fullPath.exists(), isFalse); + await storage.makeDir(dirPath); + expect(await fullPath.exists(), isTrue); + }); + + test('does not throw when directory already exists', () async { + const dirPath = 'existing_dir'; + await storage.makeDir(dirPath); + await storage.makeDir(dirPath); // Should not throw + expect(await Directory(p.join(tempDir.path, dirPath)).exists(), isTrue); + }); + }); + + group('rmDir', () { + test('recursively deletes directory with files and subdirectories', () async { + const dirPath = 'test_dir'; + final file1Path = p.join(dirPath, 'file1'); + final file2Path = p.join(dirPath, 'subdir/file2'); + final data = Uint8List.fromList([1, 2, 3]); + + await storage.saveFile(file1Path, Stream.fromIterable([data])); + await storage.saveFile(file2Path, Stream.fromIterable([data])); + + final dir = Directory(p.join(tempDir.path, dirPath)); + expect(await dir.exists(), isTrue); + + await storage.rmDir(dirPath); + expect(await dir.exists(), isFalse); + }); + + test('does not throw when directory does not exist', () async { + await storage.rmDir('non_existent_dir'); + }); + }); + + group('copyFile', () { + test('copies file to target path', () async { + const sourcePath = 'source_file'; + const targetPath = 'target_file'; + final data = Uint8List.fromList([1, 2, 3]); + final dataStream = Stream.fromIterable([data]); + + await storage.saveFile(sourcePath, dataStream); + await storage.copyFile(sourcePath, targetPath); + + final resultStream = storage.readFile(targetPath); + final result = await resultStream.toList(); + expect(result, equals([data])); + expect(await storage.fileExists(sourcePath), isTrue); // Source still exists + }); + + test('throws when source file does not exist', () async { + expect( + () => storage.copyFile('non_existent', 'target'), + throwsA(isA()), + ); + }); + + test('creates target parent directories', () async { + const sourcePath = 'source_file'; + const targetPath = 'subdir/nested/target_file'; + final data = Uint8List.fromList([1, 2, 3]); + final dataStream = Stream.fromIterable([data]); + + await storage.saveFile(sourcePath, dataStream); + await storage.copyFile(sourcePath, targetPath); + + final resultStream = storage.readFile(targetPath); + final result = await resultStream.toList(); + expect(result, equals([data])); + expect(await Directory(p.join(tempDir.path, 'subdir', 'nested')).exists(), isTrue); + }); + }); + + group('file system integration', () { + test('handles special characters in file path', () async { + const filePath = 'file with spaces & symbols!@#'; + final data = Uint8List.fromList([1, 2, 3]); + final dataStream = Stream.fromIterable([data]); + + final size = await storage.saveFile(filePath, dataStream); + expect(size, equals(data.length)); + + final resultStream = storage.readFile(filePath); + final result = await resultStream.toList(); + expect(result, equals([data])); + }); + + test('handles large binary data stream', () async { + const filePath = 'large_file'; + final data = Uint8List.fromList(List.generate(10000, (i) => i % 256)); + final chunkSize = 1000; + final chunks = []; + for (var i = 0; i < data.length; i += chunkSize) { + chunks.add(Uint8List.fromList( + data.sublist(i, i + chunkSize < data.length ? i + chunkSize : data.length))); + } + final dataStream = Stream.fromIterable(chunks); + + final size = await storage.saveFile(filePath, dataStream); + expect(size, equals(data.length)); + + final resultStream = storage.readFile(filePath); + final result = Uint8List.fromList((await resultStream.toList()).expand((chunk) => chunk).toList()); + expect(result, equals(data)); + }); + }); + + group('concurrent operations', () { + test('handles concurrent saves to different files', () async { + final futures = >[]; + final fileCount = 10; + + for (int i = 0; i < fileCount; i++) { + final data = Uint8List.fromList([i, i + 1, i + 2]); + futures.add(storage.saveFile('file_$i', Stream.fromIterable([data]))); + } + + await Future.wait(futures); + + for (int i = 0; i < fileCount; i++) { + final resultStream = storage.readFile('file_$i'); + final result = await resultStream.toList(); + expect(result, equals([Uint8List.fromList([i, i + 1, i + 2])])); + } + }); + + test('handles concurrent saves to the same file', () async { + const filePath = 'concurrent_test'; + final data1 = Uint8List.fromList([1, 2, 3]); + final data2 = Uint8List.fromList([4, 5, 6]); + final futures = [ + storage.saveFile(filePath, Stream.fromIterable([data1])), + storage.saveFile(filePath, Stream.fromIterable([data2])), + ]; + + await Future.wait(futures); + + final resultStream = storage.readFile(filePath); + final result = await resultStream.toList(); + expect(result, anyOf(equals([data1]), equals([data2]))); + }); + }); + }); +} \ No newline at end of file From f66fbdb7bcceaa1d7ceea7dc335bf430ba4c8cc4 Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Tue, 5 Aug 2025 10:58:48 +0200 Subject: [PATCH 04/18] Merge branch 'attachment-package-refactor' of github.com:powersync-ja/powersync.dart into attachment-package-refactor --- .../lib/attachments/photo_widget.dart | 13 -- .../attachments/remote_storage_adapter.dart | 2 +- .../pubspec.lock | 8 +- .../lib/powersync_attachments_stream.dart | 5 +- .../lib/src/attachment.dart | 49 +----- .../lib/src/attachment_queue_service.dart | 18 ++- .../implementations/attachment_context.dart | 53 ++---- .../lib/src/sync/syncing_service.dart | 9 +- .../powersync_attachments_stream/pubspec.lock | 17 +- .../powersync_attachments_stream/pubspec.yaml | 4 +- .../test/local_storage_test.dart | 153 ++++++++++++++---- 11 files changed, 166 insertions(+), 165 deletions(-) diff --git a/demos/supabase-todolist-new-attachment/lib/attachments/photo_widget.dart b/demos/supabase-todolist-new-attachment/lib/attachments/photo_widget.dart index fd0029e8..5d11276a 100644 --- a/demos/supabase-todolist-new-attachment/lib/attachments/photo_widget.dart +++ b/demos/supabase-todolist-new-attachment/lib/attachments/photo_widget.dart @@ -36,7 +36,6 @@ class _PhotoWidgetState extends State { final log = Logger('PhotoWidget'); Future<_ResolvedPhotoState> _getPhotoState(photoId) async { - log.info('getPhotoState: $photoId'); if (photoId == null) { return _ResolvedPhotoState(photoPath: null, fileExists: false); } @@ -45,8 +44,6 @@ class _PhotoWidgetState extends State { bool fileExists = await File(photoPath).exists(); - log.info('fileExists: $fileExists'); - final row = await attachmentQueue.db .getOptional('SELECT * FROM attachments_queue WHERE id = ?', [photoId]); @@ -54,16 +51,6 @@ class _PhotoWidgetState extends State { if (row != null) { Attachment attachment = Attachment.fromRow(row); - // Attachment attachment = Attachment.fromMap(row); - - // Log as JSON using logging package - // ignore: avoid_print - attachmentQueue.logger.info({ - 'event': 'attachment_object', - 'photoPath': photoPath, - 'fileExists': fileExists, - 'attachment': '', - }); return _ResolvedPhotoState( photoPath: photoPath, fileExists: fileExists, attachment: attachment); diff --git a/demos/supabase-todolist-new-attachment/lib/attachments/remote_storage_adapter.dart b/demos/supabase-todolist-new-attachment/lib/attachments/remote_storage_adapter.dart index a3e49d86..c804f78f 100644 --- a/demos/supabase-todolist-new-attachment/lib/attachments/remote_storage_adapter.dart +++ b/demos/supabase-todolist-new-attachment/lib/attachments/remote_storage_adapter.dart @@ -25,7 +25,7 @@ class SupabaseStorageAdapter implements RemoteStorage { fileOptions: FileOptions( contentType: attachment.mediaType ?? 'application/octet-stream')); - print('uploadFile: ${attachment.filename}'); + } catch (error) { throw Exception(error); } finally { diff --git a/demos/supabase-todolist-new-attachment/pubspec.lock b/demos/supabase-todolist-new-attachment/pubspec.lock index 0e72a5e1..8114b0f3 100644 --- a/demos/supabase-todolist-new-attachment/pubspec.lock +++ b/demos/supabase-todolist-new-attachment/pubspec.lock @@ -566,14 +566,14 @@ packages: path: "../../packages/powersync" relative: true source: path - version: "1.14.1" + version: "1.15.0" powersync_attachments_helper: dependency: "direct overridden" description: path: "../../packages/powersync_attachments_helper" relative: true source: path - version: "0.6.18+10" + version: "0.6.18+11" powersync_attachments_stream: dependency: "direct main" description: @@ -587,14 +587,14 @@ packages: path: "../../packages/powersync_core" relative: true source: path - version: "1.4.1" + version: "1.5.0" powersync_flutter_libs: dependency: "direct overridden" description: path: "../../packages/powersync_flutter_libs" relative: true source: path - version: "0.4.9" + version: "0.4.10" pub_semver: dependency: transitive description: diff --git a/packages/powersync_attachments_stream/lib/powersync_attachments_stream.dart b/packages/powersync_attachments_stream/lib/powersync_attachments_stream.dart index c39485d8..26356fc2 100644 --- a/packages/powersync_attachments_stream/lib/powersync_attachments_stream.dart +++ b/packages/powersync_attachments_stream/lib/powersync_attachments_stream.dart @@ -1,7 +1,4 @@ /// Default exports for native platforms (dart:io). For web, use 'common.dart'. export 'common.dart'; export 'src/storage/io_local_storage.dart'; -export 'src/attachment_queue_service.dart'; -export 'src/implementations/attachment_service.dart'; -export 'src/implementations/attachment_context.dart'; -export 'src/abstractions/remote_storage.dart'; \ No newline at end of file +export 'src/attachment_queue_service.dart'; \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/src/attachment.dart b/packages/powersync_attachments_stream/lib/src/attachment.dart index 065d7f5a..866111f4 100644 --- a/packages/powersync_attachments_stream/lib/src/attachment.dart +++ b/packages/powersync_attachments_stream/lib/src/attachment.dart @@ -15,10 +15,8 @@ enum AttachmentState { synced, /// The attachment is archived and no longer actively synchronized. - archived, -} + archived; -extension AttachmentStateX on AttachmentState { static AttachmentState fromInt(int value) { if (value < 0 || value >= AttachmentState.values.length) { throw ArgumentError('Invalid value for AttachmentState: $value'); @@ -54,22 +52,6 @@ class Attachment { this.metaData, }); - /// Factory constructor to create an Attachment from a database map. - /// [map] should be a `Map` from your database query. - // factory Attachment.fromMap(Map map) { - // return Attachment( - // id: map['id'] as String, - // timestamp: map['timestamp'] as int? ?? 0, - // filename: map['filename'] as String, - // localUri: map['local_uri'] as String?, - // mediaType: map['media_type'] as String?, - // size: map['size'] as int?, - // state: AttachmentStateX.fromInt(map['state'] as int), - // hasSynced: (map['has_synced'] as int? ?? 0) > 0, - // metaData: map['meta_data'] as String?, - // ); - // } - factory Attachment.fromRow(Row row) { return Attachment( id: row['id'] as String, @@ -78,15 +60,8 @@ class Attachment { localUri: row['local_uri'] as String?, mediaType: row['media_type'] as String?, size: row['size'] as int?, - // state: AttachmentState.queuedDownload, - state: AttachmentStateX.fromInt(row['state'] as int), - // state: AttachmentStateX.fromInt(row['state'] as int? ?? AttachmentState.queuedDownload.index), - // state: AttachmentState.values.firstWhere( - // (e) => e.index == row['state'], - // orElse: () => AttachmentState.queuedDownload, - // ), + state: AttachmentState.fromInt(row['state'] as int), hasSynced: (row['has_synced'] as int? ?? 0) > 0, - // metaData: 'test', metaData: row['meta_data']?.toString(), ); } @@ -115,26 +90,6 @@ class Attachment { metaData: metaData ?? this.metaData, ); } - - /// Converts this Attachment to a JSON-compatible map. - Map toJson() { - try { - return { - 'id': id, - 'timestamp': timestamp, - 'filename': filename, - 'state': state.index, - 'local_uri': localUri, - 'media_type': mediaType, - 'size': size, - 'has_synced': hasSynced ? 1 : 0, - 'meta_data': metaData, - }; - } catch (e) { - print('toJson error: $e'); - return {}; - } - } } class AttachmentsQueueTable extends Table { diff --git a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart index 27772b5d..4aae510b 100644 --- a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart +++ b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:logging/logging.dart'; +import 'package:powersync_core/powersync_core.dart'; import 'package:sqlite_async/mutex.dart'; import 'attachment.dart'; import 'abstractions/attachment_service.dart'; @@ -33,7 +34,7 @@ class WatchedAttachmentItem { /// Class used to implement the attachment queue. /// Requires a database client, remote storage adapter, and an attachment directory name. class AttachmentQueue { - final dynamic db; + final PowerSyncDatabase db; final RemoteStorage remoteStorage; final String attachmentsDirectory; final Stream> Function() watchAttachments; @@ -43,7 +44,6 @@ class AttachmentQueue { final Duration syncInterval; final int archivedCacheLimit; final Duration syncThrottleDuration; - final List? subdirectories; final bool downloadAttachments; final Logger logger; @@ -66,7 +66,6 @@ class AttachmentQueue { this.syncInterval = const Duration(seconds: 30), this.archivedCacheLimit = 100, this.syncThrottleDuration = const Duration(seconds: 1), - this.subdirectories, this.downloadAttachments = true, Logger? logger, }) : logger = logger ?? Logger('AttachmentQueue') { @@ -101,13 +100,8 @@ class AttachmentQueue { await _stopSyncingInternal(); - logger.info('startSync attachmentsDirectory: $attachmentsDirectory'); await localStorage.makeDir(attachmentsDirectory); - subdirectories?.forEach((subdirectory) async { - await localStorage.makeDir('$attachmentsDirectory/$subdirectory'); - }); - await attachmentsService.withContext((context) async { await _verifyAttachments(context); }); @@ -197,8 +191,16 @@ class AttachmentQueue { filename: filename, state: AttachmentState.queuedDownload, metaData: item.metaData, + // hasSynced: existingQueueItem.hasSynced; ), ); + + // attachmentUpdates.add( + // existingQueueItem.copyWith( + // filename: filename, + // state: AttachmentState.queuedDownload, + // ), + // ); } else if (existingQueueItem.state == AttachmentState.archived) { // The attachment is present again. Need to queue it for sync. if (existingQueueItem.hasSynced) { diff --git a/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart b/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart index 4eee509e..40ac9535 100644 --- a/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart +++ b/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart @@ -47,27 +47,9 @@ class AttachmentContextImpl implements AttachmentContext { @override Future saveAttachment(Attachment attachment) async { - Attachment updatedRecord = attachment.copyWith( - timestamp: DateTime.now().millisecondsSinceEpoch, - ); - - await db.execute( - ''' - INSERT OR REPLACE INTO $table - (id, timestamp, filename, local_uri, media_type, size, state) VALUES (?, ?, ?, ?, ?, ?, ?) - ''', - [ - updatedRecord.id, - updatedRecord.timestamp, - updatedRecord.filename, - updatedRecord.localUri, - updatedRecord.mediaType, - updatedRecord.size, - updatedRecord.state.index, - ], - ); - - return updatedRecord; + return await db.writeLock((ctx) async { + return await upsertAttachment(attachment, ctx); + }); } @override @@ -76,28 +58,11 @@ class AttachmentContextImpl implements AttachmentContext { log.info('No attachments to save.'); return; } - - final updatedRecords = attachments.map((attachment) { - final updated = attachment.copyWith( - timestamp: DateTime.now().millisecondsSinceEpoch, - ); - return [ - updated.id, - updated.filename, - updated.localUri, - updated.mediaType, - updated.size, - updated.timestamp, - updated.state.index, - ]; - }).toList(); - - log.info('Saving ${updatedRecords.length} attachments...'); - - await db.executeBatch(''' - INSERT OR REPLACE INTO $table - (id, filename, local_uri, media_type, size, timestamp, state) VALUES (?, ?, ?, ?, ?, ?, ?) - ''', updatedRecords); + await db.writeTransaction((tx) async { + for (final attachment in attachments) { + await upsertAttachment(attachment, tx); + } + }); } @override @@ -181,11 +146,11 @@ class AttachmentContextImpl implements AttachmentContext { attachment.mediaType, attachment.size, attachment.state.index, - // attachment.state, attachment.hasSynced ? 1 : 0, attachment.metaData, ], ); + return attachment; } } diff --git a/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart b/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart index bfb207c8..0c95c1c1 100644 --- a/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart +++ b/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart @@ -172,9 +172,6 @@ class SyncingService { if (attachment.localUri == null) { throw Exception('No localUri for attachment $attachment'); } - _log.info( - 'SyncingService: Calling remoteStorage.uploadFile for ${attachment.id}', - ); await remoteStorage.uploadFile( localStorage.readFile(attachment.localUri!), attachment, @@ -211,9 +208,6 @@ class SyncingService { ); final attachmentPath = await getLocalUri(attachment.filename); try { - _log.info( - 'SyncingService: Calling remoteStorage.downloadFile for ${attachment.id}', - ); final fileStream = await remoteStorage.downloadFile(attachment); await localStorage.saveFile( attachmentPath, @@ -222,6 +216,9 @@ class SyncingService { _log.info( 'SyncingService: Successfully downloaded file "${attachment.id}"', ); + + _log.info('downloadAttachmentXY $attachment'); + return attachment.copyWith( localUri: attachmentPath, state: AttachmentState.synced, diff --git a/packages/powersync_attachments_stream/pubspec.lock b/packages/powersync_attachments_stream/pubspec.lock index ae003e13..c10c760c 100644 --- a/packages/powersync_attachments_stream/pubspec.lock +++ b/packages/powersync_attachments_stream/pubspec.lock @@ -26,7 +26,7 @@ packages: source: hosted version: "2.7.0" async: - dependency: transitive + dependency: "direct main" description: name: async sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" @@ -398,10 +398,9 @@ packages: powersync_core: dependency: "direct main" description: - name: powersync_core - sha256: d8ae292bc77f0a96f44c6cc2911d1b781760a87a919e5045e75458cba83bb759 - url: "https://pub.dev" - source: hosted + path: "../powersync_core" + relative: true + source: path version: "1.5.0" pub_semver: dependency: transitive @@ -568,6 +567,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.8" + test_descriptor: + dependency: "direct dev" + description: + name: test_descriptor + sha256: "9ce468c97ae396e8440d26bb43763f84e2a2a5331813ee5a397cb4da481aaf9a" + url: "https://pub.dev" + source: hosted + version: "2.0.2" typed_data: dependency: transitive description: diff --git a/packages/powersync_attachments_stream/pubspec.yaml b/packages/powersync_attachments_stream/pubspec.yaml index 42e6f761..cdb51763 100644 --- a/packages/powersync_attachments_stream/pubspec.yaml +++ b/packages/powersync_attachments_stream/pubspec.yaml @@ -12,7 +12,6 @@ dependencies: sdk: flutter path: ^1.8.0 async: ^2.11.0 - powersync_core: ^1.4.1 logging: ^1.2.0 sqlite_async: ^0.11.0 @@ -21,5 +20,6 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^5.0.0 test: ^1.25.15 + test_descriptor: ^2.0.2 + flutter_lints: ^5.0.0 diff --git a/packages/powersync_attachments_stream/test/local_storage_test.dart b/packages/powersync_attachments_stream/test/local_storage_test.dart index c90e7121..2d75f32a 100644 --- a/packages/powersync_attachments_stream/test/local_storage_test.dart +++ b/packages/powersync_attachments_stream/test/local_storage_test.dart @@ -4,21 +4,19 @@ import 'dart:typed_data'; import 'package:test/test.dart'; import 'package:path/path.dart' as p; import 'package:powersync_attachments_stream/src/storage/io_local_storage.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; void main() { group('IOLocalStorage', () { - late Directory tempDir; late IOLocalStorage storage; setUp(() async { - tempDir = await Directory.systemTemp.createTemp('local_storage_test_'); - storage = IOLocalStorage(tempDir); + storage = IOLocalStorage(Directory(d.sandbox)); }); tearDown(() async { - if (await tempDir.exists()) { - await tempDir.delete(recursive: true); - } + // Clean up is handled automatically by test_descriptor + // No manual cleanup needed }); group('saveFile and readFile', () { @@ -33,6 +31,9 @@ void main() { final resultStream = storage.readFile(filePath); final result = await resultStream.toList(); expect(result, equals([data])); + + // Assert filesystem state using test_descriptor + await d.file(filePath, data).validate(); }); test('throws when reading non-existent file', () async { @@ -41,11 +42,14 @@ void main() { () => storage.readFile(filePath).toList(), throwsA(isA()), ); + + // Assert file does not exist using Dart's File API + expect(await File(p.join(d.sandbox, filePath)).exists(), isFalse); }); test('creates parent directories if they do not exist', () async { const filePath = 'subdir/nested/test'; - final nonExistentDir = Directory(p.join(tempDir.path, 'subdir', 'nested')); + final nonExistentDir = Directory(p.join(d.sandbox, 'subdir', 'nested')); final data = Uint8List.fromList([1, 2, 3]); final dataStream = Stream.fromIterable([data]); @@ -58,6 +62,33 @@ void main() { final resultStream = storage.readFile(filePath); final result = await resultStream.toList(); expect(result, equals([data])); + + // Assert directory structure + await d.dir('subdir/nested', [d.file('test', data)]).validate(); + }); + + test('creates all parent directories for deeply nested file', () async { + const filePath = 'a/b/c/d/e/f/g/h/i/j/testfile'; + final nestedDir = Directory( + p.join(d.sandbox, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'), + ); + final data = Uint8List.fromList([42, 43, 44]); + final dataStream = Stream.fromIterable([data]); + + expect(await nestedDir.exists(), isFalse); + + final size = await storage.saveFile(filePath, dataStream); + expect(size, equals(data.length)); + expect(await nestedDir.exists(), isTrue); + + final resultStream = storage.readFile(filePath); + final result = await resultStream.toList(); + expect(result, equals([data])); + + // Assert deep directory structure + await d.dir('a/b/c/d/e/f/g/h/i/j', [ + d.file('testfile', data), + ]).validate(); }); test('overwrites existing file', () async { @@ -74,6 +105,9 @@ void main() { final resultStream = storage.readFile(filePath); final result = await resultStream.toList(); expect(result, equals([newData])); + + // Assert file content + await d.file(filePath, newData).validate(); }); }); @@ -88,10 +122,15 @@ void main() { await storage.deleteFile(filePath); expect(await storage.fileExists(filePath), isFalse); + + // Assert file does not exist + expect(await File(p.join(d.sandbox, filePath)).exists(), isFalse); }); test('does not throw when deleting non-existent file', () async { - await storage.deleteFile('non_existent'); + const filePath = 'non_existent'; + await storage.deleteFile(filePath); + expect(await File(p.join(d.sandbox, filePath)).exists(), isFalse); }); }); @@ -103,50 +142,66 @@ void main() { await storage.saveFile(filePath, dataStream); expect(await storage.fileExists(filePath), isTrue); + + await d.file(filePath, data).validate(); }); test('returns false for non-existent file', () async { - expect(await storage.fileExists('non_existent'), isFalse); + const filePath = 'non_existent'; + expect(await storage.fileExists(filePath), isFalse); + expect(await File(p.join(d.sandbox, filePath)).exists(), isFalse); }); }); group('makeDir', () { test('creates directory and its parents', () async { const dirPath = 'test_dir/subdir'; - final fullPath = Directory(p.join(tempDir.path, dirPath)); + final fullPath = Directory(p.join(d.sandbox, dirPath)); expect(await fullPath.exists(), isFalse); await storage.makeDir(dirPath); expect(await fullPath.exists(), isTrue); + + await d.dir('test_dir/subdir').validate(); }); test('does not throw when directory already exists', () async { const dirPath = 'existing_dir'; await storage.makeDir(dirPath); await storage.makeDir(dirPath); // Should not throw - expect(await Directory(p.join(tempDir.path, dirPath)).exists(), isTrue); + expect(await Directory(p.join(d.sandbox, dirPath)).exists(), isTrue); + + await d.dir('existing_dir').validate(); }); }); group('rmDir', () { - test('recursively deletes directory with files and subdirectories', () async { - const dirPath = 'test_dir'; - final file1Path = p.join(dirPath, 'file1'); - final file2Path = p.join(dirPath, 'subdir/file2'); - final data = Uint8List.fromList([1, 2, 3]); + test( + 'recursively deletes directory with files and subdirectories', + () async { + const dirPath = 'test_dir'; + final file1Path = p.join(dirPath, 'file1'); + final file2Path = p.join(dirPath, 'subdir/file2'); + final data = Uint8List.fromList([1, 2, 3]); - await storage.saveFile(file1Path, Stream.fromIterable([data])); - await storage.saveFile(file2Path, Stream.fromIterable([data])); + await storage.saveFile(file1Path, Stream.fromIterable([data])); + await storage.saveFile(file2Path, Stream.fromIterable([data])); - final dir = Directory(p.join(tempDir.path, dirPath)); - expect(await dir.exists(), isTrue); + final dir = Directory(p.join(d.sandbox, dirPath)); + expect(await dir.exists(), isTrue); - await storage.rmDir(dirPath); - expect(await dir.exists(), isFalse); - }); + await storage.rmDir(dirPath); + expect(await dir.exists(), isFalse); + + // Assert directory does not exist + expect(await Directory(p.join(d.sandbox, dirPath)).exists(), isFalse); + }, + ); test('does not throw when directory does not exist', () async { - await storage.rmDir('non_existent_dir'); + const dirPath = 'non_existent_dir'; + await storage.rmDir(dirPath); + expect(await Directory(p.join(d.sandbox, dirPath)).exists(), isFalse); }); }); @@ -163,14 +218,20 @@ void main() { final resultStream = storage.readFile(targetPath); final result = await resultStream.toList(); expect(result, equals([data])); - expect(await storage.fileExists(sourcePath), isTrue); // Source still exists + expect(await storage.fileExists(sourcePath), isTrue); + + await d.file(targetPath, data).validate(); + await d.file(sourcePath, data).validate(); }); test('throws when source file does not exist', () async { + const sourcePath = 'non_existent'; + const targetPath = 'target'; expect( - () => storage.copyFile('non_existent', 'target'), + () => storage.copyFile(sourcePath, targetPath), throwsA(isA()), ); + expect(await File(p.join(d.sandbox, targetPath)).exists(), isFalse); }); test('creates target parent directories', () async { @@ -185,7 +246,12 @@ void main() { final resultStream = storage.readFile(targetPath); final result = await resultStream.toList(); expect(result, equals([data])); - expect(await Directory(p.join(tempDir.path, 'subdir', 'nested')).exists(), isTrue); + expect( + await Directory(p.join(d.sandbox, 'subdir', 'nested')).exists(), + isTrue, + ); + + await d.dir('subdir/nested', [d.file('target_file', data)]).validate(); }); }); @@ -201,6 +267,8 @@ void main() { final resultStream = storage.readFile(filePath); final result = await resultStream.toList(); expect(result, equals([data])); + + await d.file(filePath, data).validate(); }); test('handles large binary data stream', () async { @@ -209,8 +277,14 @@ void main() { final chunkSize = 1000; final chunks = []; for (var i = 0; i < data.length; i += chunkSize) { - chunks.add(Uint8List.fromList( - data.sublist(i, i + chunkSize < data.length ? i + chunkSize : data.length))); + chunks.add( + Uint8List.fromList( + data.sublist( + i, + i + chunkSize < data.length ? i + chunkSize : data.length, + ), + ), + ); } final dataStream = Stream.fromIterable(chunks); @@ -218,8 +292,12 @@ void main() { expect(size, equals(data.length)); final resultStream = storage.readFile(filePath); - final result = Uint8List.fromList((await resultStream.toList()).expand((chunk) => chunk).toList()); + final result = Uint8List.fromList( + (await resultStream.toList()).expand((chunk) => chunk).toList(), + ); expect(result, equals(data)); + + await d.file(filePath, data).validate(); }); }); @@ -238,7 +316,15 @@ void main() { for (int i = 0; i < fileCount; i++) { final resultStream = storage.readFile('file_$i'); final result = await resultStream.toList(); - expect(result, equals([Uint8List.fromList([i, i + 1, i + 2])])); + expect( + result, + equals([ + Uint8List.fromList([i, i + 1, i + 2]), + ]), + ); + await d + .file('file_$i', Uint8List.fromList([i, i + 1, i + 2])) + .validate(); } }); @@ -256,6 +342,11 @@ void main() { final resultStream = storage.readFile(filePath); final result = await resultStream.toList(); expect(result, anyOf(equals([data1]), equals([data2]))); + + // Assert one of the possible outcomes + final file = File(p.join(d.sandbox, filePath)); + final fileData = await file.readAsBytes(); + expect(fileData, anyOf(equals(data1), equals(data2))); }); }); }); From c2086f2d6217f82583a4532af3ca163617764f78 Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Mon, 11 Aug 2025 11:38:57 +0200 Subject: [PATCH 05/18] Refactor attachment handling and logging in the powersync_attachments_stream package. Removed unused imports, improved error handling during file uploads and downloads, and enhanced and renamed logging for better traceability of attachment operations. --- .../lib/attachments/photo_capture_widget.dart | 3 - .../lib/attachments/queue.dart | 3 +- .../attachments/remote_storage_adapter.dart | 56 ++++++++++++++----- .../lib/src/attachment_queue_service.dart | 8 +-- .../implementations/attachment_context.dart | 29 ++++++---- .../implementations/attachment_service.dart | 8 +-- .../lib/src/sync/syncing_service.dart | 55 +++++++++--------- 7 files changed, 94 insertions(+), 68 deletions(-) diff --git a/demos/supabase-todolist-new-attachment/lib/attachments/photo_capture_widget.dart b/demos/supabase-todolist-new-attachment/lib/attachments/photo_capture_widget.dart index 90c06944..3b7ed33d 100644 --- a/demos/supabase-todolist-new-attachment/lib/attachments/photo_capture_widget.dart +++ b/demos/supabase-todolist-new-attachment/lib/attachments/photo_capture_widget.dart @@ -4,10 +4,7 @@ import 'dart:typed_data'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; -import 'package:powersync/powersync.dart' as powersync; import 'package:powersync_flutter_demo_new/attachments/queue.dart'; -import 'package:powersync_flutter_demo_new/models/todo_item.dart'; -import 'package:powersync_flutter_demo_new/powersync.dart'; class TakePhotoWidget extends StatefulWidget { final String todoId; diff --git a/demos/supabase-todolist-new-attachment/lib/attachments/queue.dart b/demos/supabase-todolist-new-attachment/lib/attachments/queue.dart index 52810c7a..e4e7b170 100644 --- a/demos/supabase-todolist-new-attachment/lib/attachments/queue.dart +++ b/demos/supabase-todolist-new-attachment/lib/attachments/queue.dart @@ -38,9 +38,8 @@ Future initializeAttachmentQueue(PowerSyncDatabase db) async { return items; }), localStorage: localStorage, - errorHandler: null, // Optionally implement SyncErrorHandler + errorHandler: null, ); - // await attachmentQueue.startSync(); } Future savePhotoAttachment(Stream photoData, String todoId, diff --git a/demos/supabase-todolist-new-attachment/lib/attachments/remote_storage_adapter.dart b/demos/supabase-todolist-new-attachment/lib/attachments/remote_storage_adapter.dart index c804f78f..2965b015 100644 --- a/demos/supabase-todolist-new-attachment/lib/attachments/remote_storage_adapter.dart +++ b/demos/supabase-todolist-new-attachment/lib/attachments/remote_storage_adapter.dart @@ -3,22 +3,47 @@ import 'dart:typed_data'; import 'package:powersync_attachments_stream/powersync_attachments_stream.dart'; import 'package:powersync_flutter_demo_new/app_config.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; -import 'package:image/image.dart' as img; +import 'package:logging/logging.dart'; class SupabaseStorageAdapter implements RemoteStorage { + static final _log = Logger('SupabaseStorageAdapter'); + @override Future uploadFile( Stream> fileData, Attachment attachment) async { _checkSupabaseBucketIsConfigured(); - final tempFile = - File('${Directory.systemTemp.path}/${attachment.filename}'); - final sink = tempFile.openWrite(); + + // Check if attachment size is specified (required for buffer allocation) + final byteSize = attachment.size; + if (byteSize == null) { + throw Exception('Cannot upload a file with no byte size specified'); + } + + _log.info('uploadFile: ${attachment.filename} (size: $byteSize bytes)'); + + // Collect all stream data into a single Uint8List buffer (like Kotlin version) + final buffer = Uint8List(byteSize); + var position = 0; + await for (final chunk in fileData) { - sink.add(chunk); + if (position + chunk.length > byteSize) { + throw Exception('File data exceeds specified size'); + } + buffer.setRange(position, position + chunk.length, chunk); + position += chunk.length; } - await sink.close(); - print('uploadFile: ${attachment.filename}'); + + if (position != byteSize) { + throw Exception( + 'File data size ($position) does not match specified size ($byteSize)'); + } + + // Create a temporary file from the buffer for upload + final tempFile = + File('${Directory.systemTemp.path}/${attachment.filename}'); try { + await tempFile.writeAsBytes(buffer); + await Supabase.instance.client.storage .from(AppConfig.supabaseStorageBucket) .upload(attachment.filename, tempFile, @@ -26,7 +51,9 @@ class SupabaseStorageAdapter implements RemoteStorage { contentType: attachment.mediaType ?? 'application/octet-stream')); + _log.info('Successfully uploaded ${attachment.filename}'); } catch (error) { + _log.severe('Error uploading ${attachment.filename}', error); throw Exception(error); } finally { if (await tempFile.exists()) { @@ -38,23 +65,26 @@ class SupabaseStorageAdapter implements RemoteStorage { @override Future>> downloadFile(Attachment attachment) async { _checkSupabaseBucketIsConfigured(); - print('downloadFile: ${attachment.filename}'); try { + _log.info('downloadFile: ${attachment.filename}'); + Uint8List fileBlob = await Supabase.instance.client.storage .from(AppConfig.supabaseStorageBucket) .download(attachment.filename); - final image = img.decodeImage(fileBlob); - Uint8List blob = img.JpegEncoder().encode(image!); - print('downloadFile: ${blob.length}'); - return Stream.value(blob); + + _log.info( + 'Successfully downloaded ${attachment.filename} (${fileBlob.length} bytes)'); + + // Return the raw file data as a stream + return Stream.value(fileBlob); } catch (error) { + _log.severe('Error downloading ${attachment.filename}', error); throw Exception(error); } } @override Future deleteFile(Attachment attachment) async { - print('deleteFile: ${attachment.filename}'); _checkSupabaseBucketIsConfigured(); try { await Supabase.instance.client.storage diff --git a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart index 4aae510b..58e3fde7 100644 --- a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart +++ b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart @@ -71,7 +71,7 @@ class AttachmentQueue { }) : logger = logger ?? Logger('AttachmentQueue') { attachmentsService = AttachmentServiceImpl( db: db, - log: logger ?? Logger('AttachmentQueue'), + logger: logger ?? Logger('AttachmentQueue'), maxArchivedCount: archivedCacheLimit, attachmentsQueueTableName: attachmentsQueueTableName, ); @@ -195,12 +195,6 @@ class AttachmentQueue { ), ); - // attachmentUpdates.add( - // existingQueueItem.copyWith( - // filename: filename, - // state: AttachmentState.queuedDownload, - // ), - // ); } else if (existingQueueItem.state == AttachmentState.archived) { // The attachment is present again. Need to queue it for sync. if (existingQueueItem.hasSynced) { diff --git a/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart b/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart index 40ac9535..46328c4a 100644 --- a/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart +++ b/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart @@ -101,30 +101,35 @@ class AttachmentContextImpl implements AttachmentContext { Future deleteArchivedAttachments( Future Function(List) callback, ) async { - // Find all archived attachments - final results = await db.getAll('SELECT * FROM $table WHERE state = ?', [ - AttachmentState.archived.index, - ]); - final archivedAttachments = results - .map((row) => Attachment.fromRow(row)) - .toList(); + // Only delete archived attachments exceeding the maxArchivedCount, ordered by timestamp DESC + const limit = 1000; + final results = await db.getAll( + 'SELECT * FROM $table WHERE state = ? ORDER BY timestamp DESC LIMIT ? OFFSET ?', + [ + AttachmentState.archived.index, + limit, + maxArchivedCount, + ], + ); + final archivedAttachments = results.map((row) => Attachment.fromRow(row)).toList(); if (archivedAttachments.isEmpty) { return false; } - log.info('Deleting ${archivedAttachments.length} archived attachments...'); + log.info('Deleting ${archivedAttachments.length} archived attachments (exceeding maxArchivedCount=$maxArchivedCount)...'); // Call the callback with the list of archived attachments before deletion await callback(archivedAttachments); // Delete the archived attachments from the table final ids = archivedAttachments.map((a) => a.id).toList(); - // Use a batch delete for efficiency - final placeholders = List.filled(ids.length, '?').join(','); - await db.execute('DELETE FROM $table WHERE id IN ($placeholders)', ids); + if (ids.isNotEmpty) { + final placeholders = List.filled(ids.length, '?').join(','); + await db.execute('DELETE FROM $table WHERE id IN ($placeholders)', ids); + } log.info('Deleted ${archivedAttachments.length} archived attachments.'); - return true; + return archivedAttachments.length < limit; } @override diff --git a/packages/powersync_attachments_stream/lib/src/implementations/attachment_service.dart b/packages/powersync_attachments_stream/lib/src/implementations/attachment_service.dart index feefa115..72332f0d 100644 --- a/packages/powersync_attachments_stream/lib/src/implementations/attachment_service.dart +++ b/packages/powersync_attachments_stream/lib/src/implementations/attachment_service.dart @@ -9,7 +9,7 @@ import 'attachment_context.dart'; class AttachmentServiceImpl implements AttachmentService { final PowerSyncDatabase db; - final Logger log; + final Logger logger; final int maxArchivedCount; final String attachmentsQueueTableName; Future _mutex = Future.value(); @@ -18,16 +18,16 @@ class AttachmentServiceImpl implements AttachmentService { AttachmentServiceImpl({ required this.db, - required this.log, + required this.logger, required this.maxArchivedCount, required this.attachmentsQueueTableName, }) { - _context = AttachmentContextImpl(db, log, maxArchivedCount, attachmentsQueueTableName); + _context = AttachmentContextImpl(db, logger, maxArchivedCount, attachmentsQueueTableName); } @override Stream watchActiveAttachments() async* { - log.info('Watching attachments...'); + logger.info('Watching attachments...'); // Watch for attachments with active states (queued for upload, download, or delete) final stream = db.watch( diff --git a/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart b/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart index 0c95c1c1..8ddccdbc 100644 --- a/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart +++ b/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart @@ -18,7 +18,7 @@ class SyncingService { final SyncErrorHandler? errorHandler; final Duration syncThrottle; final Duration period; - final _log = Logger('SyncingService'); + final Logger logger; StreamSubscription? _syncSubscription; StreamSubscription? _periodicSubscription; @@ -33,7 +33,8 @@ class SyncingService { this.errorHandler, this.syncThrottle = const Duration(seconds: 5), this.period = const Duration(seconds: 30), - }); + Logger? logger, + }) : logger = logger ?? Logger('SyncingService'); Future startSync() async { if (_isClosed) return; @@ -52,10 +53,10 @@ class SyncingService { try { await attachmentsService.withContext((context) async { final attachments = await context.getActiveAttachments(); - _log.info( + logger.info( 'active attachments: ${attachments.map((e) => e.id).toList()}', ); - _log.info( + logger.info( 'SyncingService: Found ${attachments.length} active attachments', ); await handleSync(attachments, context); @@ -63,7 +64,7 @@ class SyncingService { }); } catch (e, st) { if (e is! StateError && e.toString().contains('cancelled')) { - _log.severe( + logger.severe( 'Caught exception when processing attachments', e, st, @@ -78,7 +79,7 @@ class SyncingService { // Start periodic sync _periodicSubscription = Stream.periodic(period).listen((_) { - _log.info('Periodically syncing attachments'); + logger.info('Periodically syncing attachments'); triggerSync(); }); } @@ -113,42 +114,42 @@ class SyncingService { List attachments, AttachmentContext context, ) async { - _log.info( + logger.info( 'SyncingService: Starting handleSync with ${attachments.length} attachments', ); final updatedAttachments = []; for (final attachment in attachments) { - _log.info( + logger.info( 'SyncingService: Processing attachment ${attachment.id} with state: ${attachment.state}', ); try { switch (attachment.state) { case AttachmentState.queuedDownload: - _log.info('SyncingService: Downloading [${attachment.filename}]'); + logger.info('SyncingService: Downloading [${attachment.filename}]'); updatedAttachments.add(await downloadAttachment(attachment)); break; case AttachmentState.queuedUpload: - _log.info('SyncingService: Uploading [${attachment.filename}]'); + logger.info('SyncingService: Uploading [${attachment.filename}]'); updatedAttachments.add(await uploadAttachment(attachment)); break; case AttachmentState.queuedDelete: - _log.info('SyncingService: Deleting [${attachment.filename}]'); + logger.info('SyncingService: Deleting [${attachment.filename}]'); updatedAttachments.add(await deleteAttachment(attachment)); break; case AttachmentState.synced: - _log.info( + logger.info( 'SyncingService: Attachment ${attachment.id} is already synced', ); break; case AttachmentState.archived: - _log.info( + logger.info( 'SyncingService: Attachment ${attachment.id} is archived', ); break; } } catch (e, st) { - _log.warning( + logger.warning( 'SyncingService: Error during sync for ${attachment.id}', e, st, @@ -157,7 +158,7 @@ class SyncingService { } if (updatedAttachments.isNotEmpty) { - _log.info( + logger.info( 'SyncingService: Saving ${updatedAttachments.length} updated attachments', ); await context.saveAttachments(updatedAttachments); @@ -165,7 +166,7 @@ class SyncingService { } Future uploadAttachment(Attachment attachment) async { - _log.info( + logger.info( 'SyncingService: Starting upload for attachment ${attachment.id}', ); try { @@ -176,7 +177,7 @@ class SyncingService { localStorage.readFile(attachment.localUri!), attachment, ); - _log.info( + logger.info( 'SyncingService: Successfully uploaded attachment "${attachment.id}" to Cloud Storage', ); return attachment.copyWith( @@ -184,7 +185,7 @@ class SyncingService { hasSynced: true, ); } catch (e, st) { - _log.warning( + logger.warning( 'SyncingService: Upload attachment error for attachment $attachment', e, st, @@ -192,7 +193,7 @@ class SyncingService { if (errorHandler != null) { final shouldRetry = await errorHandler!.onUploadError(attachment, e); if (!shouldRetry) { - _log.info( + logger.info( 'SyncingService: Attachment with ID ${attachment.id} has been archived', ); return attachment.copyWith(state: AttachmentState.archived); @@ -203,7 +204,7 @@ class SyncingService { } Future downloadAttachment(Attachment attachment) async { - _log.info( + logger.info( 'SyncingService: Starting download for attachment ${attachment.id}', ); final attachmentPath = await getLocalUri(attachment.filename); @@ -213,11 +214,11 @@ class SyncingService { attachmentPath, fileStream.map((chunk) => Uint8List.fromList(chunk)), ); - _log.info( + logger.info( 'SyncingService: Successfully downloaded file "${attachment.id}"', ); - _log.info('downloadAttachmentXY $attachment'); + logger.info('downloadAttachmentXY $attachment'); return attachment.copyWith( localUri: attachmentPath, @@ -228,13 +229,13 @@ class SyncingService { if (errorHandler != null) { final shouldRetry = await errorHandler!.onDownloadError(attachment, e); if (!shouldRetry) { - _log.info( + logger.info( 'SyncingService: Attachment with ID ${attachment.id} has been archived', ); return attachment.copyWith(state: AttachmentState.archived); } } - _log.warning( + logger.warning( 'SyncingService: Download attachment error for attachment $attachment', e, st, @@ -245,7 +246,7 @@ class SyncingService { Future deleteAttachment(Attachment attachment) async { try { - _log.info( + logger.info( 'SyncingService: Deleting attachment ${attachment.id} from remote storage', ); await remoteStorage.deleteFile(attachment); @@ -259,11 +260,11 @@ class SyncingService { if (errorHandler != null) { final shouldRetry = await errorHandler!.onDeleteError(attachment, e); if (!shouldRetry) { - _log.info('Attachment with ID ${attachment.id} has been archived'); + logger.info('Attachment with ID ${attachment.id} has been archived'); return attachment.copyWith(state: AttachmentState.archived); } } - _log.warning('Error deleting attachment: $e', e, st); + logger.warning('Error deleting attachment: $e', e, st); return attachment; } } From db6c4102ecaa73d19d579c9b31fa5223defebe49 Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Mon, 11 Aug 2025 12:06:58 +0200 Subject: [PATCH 06/18] Added comprehensive comments and descriptions for key classes and methods, improving clarity on attachment lifecycle management, state handling, and local storage operations. --- .../src/abstractions/attachment_context.dart | 36 ++++++++++++- .../src/abstractions/attachment_service.dart | 8 ++- .../lib/src/attachment.dart | 38 ++++++++++++++ .../lib/src/attachment_queue_service.dart | 37 +++++++++++++- .../implementations/attachment_context.dart | 4 +- .../lib/src/storage/io_local_storage.dart | 30 ++++++++++- .../lib/src/sync/syncing_service.dart | 50 ++++++++++++++++++- 7 files changed, 196 insertions(+), 7 deletions(-) diff --git a/packages/powersync_attachments_stream/lib/src/abstractions/attachment_context.dart b/packages/powersync_attachments_stream/lib/src/abstractions/attachment_context.dart index fc147375..34e76c25 100644 --- a/packages/powersync_attachments_stream/lib/src/abstractions/attachment_context.dart +++ b/packages/powersync_attachments_stream/lib/src/abstractions/attachment_context.dart @@ -1,15 +1,49 @@ import '../attachment.dart'; +/// Context for performing Attachment operations. +/// +/// This is typically provided through a locking/exclusivity method and allows +/// safe, transactional operations on the attachment queue. abstract class AttachmentContext { - Future deleteAttachment(String id, dynamic tx); + /// Delete the attachment from the attachment queue. + /// + /// [id]: The ID of the attachment to delete. + /// [tx]: The database context to use for the operation. + Future deleteAttachment(String id, dynamic context); + + /// Set the state of the attachment to ignore. Future ignoreAttachment(String id); + + /// Get the attachment from the attachment queue using an ID. Future getAttachment(String id); + + /// Save the attachment to the attachment queue. Future saveAttachment(Attachment attachment); + + /// Save the attachments to the attachment queue. Future saveAttachments(List attachments); + + /// Get all the IDs of attachments in the attachment queue. Future> getAttachmentIds(); + + /// Get all Attachment records present in the database. Future> getAttachments(); + + /// Gets all the active attachments which require an operation to be performed. Future> getActiveAttachments(); + + /// Helper function to clear the attachment queue. Currently only used for testing purposes. Future clearQueue(); + + /// Delete attachments which have been archived. + /// + /// Returns true if all items have been deleted. Returns false if there might be more archived items remaining. Future deleteArchivedAttachments(Future Function(List) callback); + + /// Upserts an attachment record given a database connection context. + /// + /// [attachment]: The attachment to upsert. + /// [context]: The database transaction/context to use for the operation. + /// Returns the upserted [Attachment]. Future upsertAttachment(Attachment attachment, dynamic context); } \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/src/abstractions/attachment_service.dart b/packages/powersync_attachments_stream/lib/src/abstractions/attachment_service.dart index a0c085ad..71e67c86 100644 --- a/packages/powersync_attachments_stream/lib/src/abstractions/attachment_service.dart +++ b/packages/powersync_attachments_stream/lib/src/abstractions/attachment_service.dart @@ -1,6 +1,12 @@ import 'attachment_context.dart'; +/// Service for interacting with the local attachment records. abstract class AttachmentService { + /// Watcher for changes to attachments table. + /// Once a change is detected it will initiate a sync of the attachments. Stream watchActiveAttachments(); - Future withContext(Future Function(AttachmentContext ctx) action); + + /// Executes a callback with an exclusive lock on all attachment operations. + /// This helps prevent race conditions between different updates. + Future withContext(Future Function(AttachmentContext context) action); } \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/src/attachment.dart b/packages/powersync_attachments_stream/lib/src/attachment.dart index 866111f4..60b84a91 100644 --- a/packages/powersync_attachments_stream/lib/src/attachment.dart +++ b/packages/powersync_attachments_stream/lib/src/attachment.dart @@ -1,6 +1,11 @@ +/// Defines attachment states and the Attachment model for the PowerSync attachments system. +/// +/// Includes metadata, state, and utility methods for working with attachments. + import 'package:powersync_core/sqlite3_common.dart' show Row; import 'package:powersync_core/powersync_core.dart'; +/// Represents the state of an attachment. enum AttachmentState { /// The attachment is queued for download from the remote storage. queuedDownload, @@ -17,6 +22,9 @@ enum AttachmentState { /// The attachment is archived and no longer actively synchronized. archived; + /// Constructs an [AttachmentState] from the corresponding integer value. + /// + /// Throws [ArgumentError] if the value does not match any [AttachmentState]. static AttachmentState fromInt(int value) { if (value < 0 || value >= AttachmentState.values.length) { throw ArgumentError('Invalid value for AttachmentState: $value'); @@ -24,22 +32,47 @@ enum AttachmentState { return AttachmentState.values[value]; } + /// Returns the ordinal value of this [AttachmentState]. int toInt() => index; } const defaultAttachmentsQueueTableName = 'attachments_queue'; +/// Represents an attachment with metadata and state information. +/// +/// {@category Attachments} +/// +/// Properties: +/// - [id]: Unique identifier for the attachment. +/// - [timestamp]: Timestamp of the last record update. +/// - [filename]: Name of the attachment file, e.g., `[id].jpg`. +/// - [state]: Current state of the attachment, represented as an ordinal of [AttachmentState]. +/// - [localUri]: Local URI pointing to the attachment file, if available. +/// - [mediaType]: Media type of the attachment, typically represented as a MIME type. +/// - [size]: Size of the attachment in bytes, if available. +/// - [hasSynced]: Indicates whether the attachment has been synced locally before. +/// - [metaData]: Additional metadata associated with the attachment. class Attachment { + /// Unique identifier for the attachment. final String id; + /// Timestamp of the last record update. final int timestamp; + /// Name of the attachment file, e.g., `[id].jpg`. final String filename; + /// Current state of the attachment, represented as an ordinal of [AttachmentState]. final AttachmentState state; + /// Local URI pointing to the attachment file, if available. final String? localUri; + /// Media type of the attachment, typically represented as a MIME type. final String? mediaType; + /// Size of the attachment in bytes, if available. final int? size; + /// Indicates whether the attachment has been synced locally before. final bool hasSynced; + /// Additional metadata associated with the attachment. final String? metaData; + /// Creates an [Attachment] instance. const Attachment({ required this.id, this.timestamp = 0, @@ -52,6 +85,10 @@ class Attachment { this.metaData, }); + /// Creates an [Attachment] instance from a database row. + /// + /// [row]: The [Row] containing attachment data. + /// Returns an [Attachment] instance populated with data from the row. factory Attachment.fromRow(Row row) { return Attachment( id: row['id'] as String, @@ -92,6 +129,7 @@ class Attachment { } } +/// Table definition for the attachments queue. class AttachmentsQueueTable extends Table { AttachmentsQueueTable({ String attachmentsQueueTableName = defaultAttachmentsQueueTableName, diff --git a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart index 58e3fde7..ee69c81e 100644 --- a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart +++ b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart @@ -1,3 +1,9 @@ +// Implements the attachment queue for PowerSync attachments. +// +// This class manages the lifecycle of attachment records, including watching for new attachments, +// syncing with remote storage, handling uploads, downloads, and deletes, and managing local storage. +// It provides hooks for error handling, cache management, and custom filename resolution. + import 'dart:async'; import 'dart:typed_data'; import 'package:logging/logging.dart'; @@ -13,13 +19,26 @@ import 'implementations/attachment_service.dart'; import 'sync/syncing_service.dart'; /// A watched attachment record item. +/// /// This is usually returned from watching all relevant attachment IDs. +/// +/// - [id]: Id for the attachment record. +/// - [fileExtension]: File extension used to determine an internal filename for storage if no [filename] is provided. +/// - [filename]: Filename to store the attachment with. +/// - [metaData]: Optional metadata for the attachment record. class WatchedAttachmentItem { + /// Id for the attachment record. final String id; + /// File extension used to determine an internal filename for storage if no [filename] is provided. final String? fileExtension; + /// Filename to store the attachment with. final String? filename; + /// Optional metadata for the attachment record. final String? metaData; + /// Creates a [WatchedAttachmentItem]. + /// + /// Either [fileExtension] or [filename] must be provided. WatchedAttachmentItem({ required this.id, this.fileExtension, @@ -32,7 +51,23 @@ class WatchedAttachmentItem { } /// Class used to implement the attachment queue. -/// Requires a database client, remote storage adapter, and an attachment directory name. +/// +/// Manages the lifecycle of attachment records, including watching for new attachments, +/// syncing with remote storage, handling uploads, downloads, and deletes, and managing local storage. +/// +/// Properties: +/// - [db]: PowerSync database client. +/// - [remoteStorage]: Adapter which interfaces with the remote storage backend. +/// - [attachmentsDirectory]: Directory name where attachment files will be written to disk. +/// - [watchAttachments]: A stream generator for the current state of local attachments. +/// - [localStorage]: Provides access to local filesystem storage methods. +/// - [attachmentsQueueTableName]: SQLite table where attachment state will be recorded. +/// - [errorHandler]: Attachment operation error handler. Specifies if failed attachment operations should be retried. +/// - [syncInterval]: Periodic interval to trigger attachment sync operations. +/// - [archivedCacheLimit]: Defines how many archived records are retained as a cache. +/// - [syncThrottleDuration]: Throttles remote sync operations triggering. +/// - [downloadAttachments]: Should attachments be downloaded. +/// - [logger]: Logging interface used for all log operations. class AttachmentQueue { final PowerSyncDatabase db; final RemoteStorage remoteStorage; diff --git a/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart b/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart index 46328c4a..6d71c38c 100644 --- a/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart +++ b/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart @@ -23,9 +23,9 @@ class AttachmentContextImpl implements AttachmentContext { } @override - Future deleteAttachment(String id, dynamic tx) async { + Future deleteAttachment(String id, dynamic context) async { log.info('deleteAttachment: $id'); - await tx.execute('DELETE FROM $table WHERE id = ?', [id]); + await context.execute('DELETE FROM $table WHERE id = ?', [id]); } @override diff --git a/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart b/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart index ea28ef50..f129e0aa 100644 --- a/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart +++ b/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart @@ -1,9 +1,27 @@ +/// Local storage adapter for handling file operations on the device filesystem. +/// +/// This file implements the [IOLocalStorage] class, which provides methods for +/// saving, reading, deleting, copying, and managing files and directories using +/// the Dart IO library. It is used as the local storage backend for attachments +/// in the PowerSync attachments system. +/// +/// Features: +/// - Save files from streams (creates directories and all necessary parents dynamically if they do not exist) +/// - Read files as streams +/// - Delete files and their metadata +/// - Copy files and their metadata +/// - Create and remove directories (creates all necessary parents dynamically) +/// - Check file existence + import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; import 'package:path/path.dart' as p; import '../abstractions/local_storage.dart'; +/// Implements [LocalStorage] for device filesystem using Dart IO. +/// +/// Handles file and directory operations for attachments. class IOLocalStorage implements LocalStorage { final Directory baseDir; @@ -11,8 +29,11 @@ class IOLocalStorage implements LocalStorage { File _fileFor(String filePath) => File(p.join(baseDir.path, filePath)); File _metaFileFor(String filePath) => - File(p.join(baseDir.path, ' [200m$filePath.meta.json [201m')); + File(p.join(baseDir.path, '$filePath.meta.json')); + /// Saves a file from a stream of [Uint8List] chunks. + /// Creates the file's directory and all necessary parent directories dynamically if they do not exist. + /// Returns the total number of bytes written. @override Future saveFile(String filePath, Stream data) async { final file = _fileFor(filePath); @@ -31,6 +52,8 @@ class IOLocalStorage implements LocalStorage { return totalSize; } + /// Reads a file as a stream of [Uint8List] chunks. + /// Throws if the file does not exist. @override Stream readFile(String filePath, {String? mediaType}) async* { final file = _fileFor(filePath); @@ -43,6 +66,7 @@ class IOLocalStorage implements LocalStorage { } } + /// Deletes a file and its metadata file. @override Future deleteFile(String filePath) async { final file = _fileFor(filePath); @@ -55,16 +79,19 @@ class IOLocalStorage implements LocalStorage { } } + /// Checks if a file exists. @override Future fileExists(String filePath) async { return await _fileFor(filePath).exists(); } + /// Creates a directory and all necessary parent directories dynamically if they do not exist. @override Future makeDir(String path) async { await Directory(p.join(baseDir.path, path)).create(recursive: true); } + /// Recursively removes a directory and all its contents. @override Future rmDir(String path) async { final dir = Directory(p.join(baseDir.path, path)); @@ -80,6 +107,7 @@ class IOLocalStorage implements LocalStorage { } } + /// Copies a file and its metadata to a new location. @override Future copyFile(String sourcePath, String targetPath) async { final sourceFile = _fileFor(sourcePath); diff --git a/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart b/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart index 8ddccdbc..33d1ef53 100644 --- a/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart +++ b/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart @@ -1,3 +1,12 @@ +// Service responsible for syncing attachments between local and remote storage. +// +// This service handles downloading, uploading, and deleting attachments, as well as +// periodically syncing attachment states. It ensures proper lifecycle management +// of sync operations and provides mechanisms for error handling and retries. +// +// The class provides a default implementation for syncing operations, which can be +// extended or customized as needed. + import 'dart:async'; import 'dart:typed_data'; import 'package:logging/logging.dart'; @@ -10,6 +19,18 @@ import '../abstractions/local_storage.dart'; import '../abstractions/remote_storage.dart'; import '../sync_error_handler.dart'; +/// SyncingService is responsible for syncing attachments between local and remote storage. +/// +/// This service handles downloading, uploading, and deleting attachments, as well as +/// periodically syncing attachment states. It ensures proper lifecycle management +/// of sync operations and provides mechanisms for error handling and retries. +/// +/// Properties: +/// - [remoteStorage]: The remote storage implementation for handling file operations. +/// - [localStorage]: The local storage implementation for managing files locally. +/// - [attachmentsService]: The service for managing attachment states and operations. +/// - [getLocalUri]: A function to resolve the local URI for a given filename. +/// - [onDownloadError], [onUploadError], [onDeleteError]: Optional error handlers for managing sync-related errors. class SyncingService { final RemoteStorage remoteStorage; final LocalStorage localStorage; @@ -36,7 +57,10 @@ class SyncingService { Logger? logger, }) : logger = logger ?? Logger('SyncingService'); - Future startSync() async { + /// Starts the syncing process, including periodic and event-driven sync operations. + /// + /// [period] is the interval at which periodic sync operations are triggered. + Future startSync({Duration period = const Duration(seconds: 30)}) async { if (_isClosed) return; _syncSubscription?.cancel(); @@ -94,22 +118,30 @@ class SyncingService { ); } + /// Enqueues a sync operation (manual trigger). Future triggerSync() async { if (_isClosed) return; _syncTriggerController.add(null); } + /// Stops all ongoing sync operations. Future stopSync() async { await _syncSubscription?.cancel(); await _periodicSubscription?.cancel(); } + /// Closes the syncing service, stopping all operations and releasing resources. Future close() async { _isClosed = true; await stopSync(); await _syncTriggerController.close(); } + /// Handles syncing operations for a list of attachments, including downloading, + /// uploading, and deleting files based on their states. + /// + /// [attachments]: The list of attachments to process. + /// [context]: The attachment context used for managing attachment states. Future handleSync( List attachments, AttachmentContext context, @@ -165,6 +197,10 @@ class SyncingService { } } + /// Uploads an attachment from local storage to remote storage. + /// + /// [attachment]: The attachment to upload. + /// Returns the updated attachment with its new state. Future uploadAttachment(Attachment attachment) async { logger.info( 'SyncingService: Starting upload for attachment ${attachment.id}', @@ -203,6 +239,10 @@ class SyncingService { } } + /// Downloads an attachment from remote storage and saves it to local storage. + /// + /// [attachment]: The attachment to download. + /// Returns the updated attachment with its new state. Future downloadAttachment(Attachment attachment) async { logger.info( 'SyncingService: Starting download for attachment ${attachment.id}', @@ -244,6 +284,10 @@ class SyncingService { } } + /// Deletes an attachment from remote and local storage, and removes it from the queue. + /// + /// [attachment]: The attachment to delete. + /// Returns the updated attachment with its new state. Future deleteAttachment(Attachment attachment) async { try { logger.info( @@ -269,6 +313,10 @@ class SyncingService { } } + /// Deletes archived attachments from local storage. + /// + /// [context]: The attachment context used to retrieve and manage archived attachments. + /// Returns `true` if all archived attachments were successfully deleted, `false` otherwise. Future deleteArchivedAttachments(AttachmentContext context) async { return context.deleteArchivedAttachments((pendingDelete) async { for (final attachment in pendingDelete) { From e51836a65b51a0d71ddcab3f7edc70e19b6663f0 Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Mon, 11 Aug 2025 14:35:25 +0200 Subject: [PATCH 07/18] Removed supabase-todolist-new-attachment demo and added the new atatchments package implementation to supabase-todolist demo. --- .../.gitignore | 52 - .../.metadata | 45 - .../supabase-todolist-new-attachment/LICENSE | 121 -- .../README.md | 64 - .../analysis_options.yaml | 29 - .../android/.gitignore | 14 - .../android/app/build.gradle.kts | 44 - .../android/app/src/debug/AndroidManifest.xml | 7 - .../android/app/src/main/AndroidManifest.xml | 45 - .../powersync_flutter_demo/MainActivity.kt | 5 - .../res/drawable-v21/launch_background.xml | 12 - .../main/res/drawable/launch_background.xml | 12 - .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 544 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 442 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 721 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 1031 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 1443 -> 0 bytes .../app/src/main/res/values-night/styles.xml | 18 - .../app/src/main/res/values/styles.xml | 18 - .../app/src/profile/AndroidManifest.xml | 7 - .../android/build.gradle.kts | 21 - .../android/gradle.properties | 3 - .../gradle/wrapper/gradle-wrapper.properties | 5 - .../android/settings.gradle.kts | 25 - .../database.sql | 71 -- .../flutter_01.png | Bin 73026 -> 0 bytes .../ios/.gitignore | 34 - .../ios/Flutter/AppFrameworkInfo.plist | 26 - .../ios/Flutter/Debug.xcconfig | 2 - .../ios/Flutter/Release.xcconfig | 2 - .../ios/Podfile | 48 - .../ios/Podfile.lock | 89 -- .../ios/Runner.xcodeproj/project.pbxproj | 552 --------- .../contents.xcworkspacedata | 7 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/WorkspaceSettings.xcsettings | 8 - .../xcshareddata/xcschemes/Runner.xcscheme | 90 -- .../contents.xcworkspacedata | 10 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/WorkspaceSettings.xcsettings | 8 - .../ios/Runner/AppDelegate.swift | 13 - .../AppIcon.appiconset/Contents.json | 122 -- .../Icon-App-1024x1024@1x.png | Bin 10932 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 295 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 406 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 450 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 282 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 462 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 704 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 406 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 586 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 862 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 862 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 1674 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 762 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 1226 -> 0 bytes .../Icon-App-83.5x83.5@2x.png | Bin 1418 -> 0 bytes .../LaunchImage.imageset/Contents.json | 23 - .../LaunchImage.imageset/LaunchImage.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/README.md | 5 - .../Runner/Base.lproj/LaunchScreen.storyboard | 37 - .../ios/Runner/Base.lproj/Main.storyboard | 26 - .../ios/Runner/Info.plist | 55 - .../ios/Runner/Runner-Bridging-Header.h | 1 - .../ios/RunnerTests/RunnerTests.swift | 12 - .../lib/app_config_template.dart | 9 - .../lib/attachments/camera_helpers.dart | 20 - .../lib/attachments/photo_capture_widget.dart | 93 -- .../lib/attachments/photo_widget.dart | 139 --- .../lib/attachments/queue.dart | 73 -- .../attachments/remote_storage_adapter.dart | 104 -- .../lib/fts_helpers.dart | 17 - .../lib/main.dart | 132 -- .../lib/migrations/fts_setup.dart | 76 -- .../lib/migrations/helpers.dart | 38 - .../lib/models/schema.dart | 27 - .../lib/models/todo_item.dart | 51 - .../lib/models/todo_list.dart | 108 -- .../lib/powersync.dart | 200 ---- .../lib/supabase.dart | 10 - .../lib/widgets/fts_search_delegate.dart | 111 -- .../lib/widgets/guard_by_sync.dart | 53 - .../lib/widgets/list_item.dart | 59 - .../lib/widgets/list_item_dialog.dart | 60 - .../lib/widgets/lists_page.dart | 70 -- .../lib/widgets/login_page.dart | 126 -- .../lib/widgets/query_widget.dart | 99 -- .../lib/widgets/resultset_table.dart | 39 - .../lib/widgets/signup_page.dart | 120 -- .../lib/widgets/status_app_bar.dart | 74 -- .../lib/widgets/todo_item_dialog.dart | 65 - .../lib/widgets/todo_item_widget.dart | 69 -- .../lib/widgets/todo_list_page.dart | 68 -- .../linux/.gitignore | 1 - .../linux/CMakeLists.txt | 138 --- .../linux/flutter/CMakeLists.txt | 88 -- .../flutter/generated_plugin_registrant.cc | 27 - .../flutter/generated_plugin_registrant.h | 15 - .../linux/flutter/generated_plugins.cmake | 27 - .../linux/main.cc | 6 - .../linux/my_application.cc | 104 -- .../linux/my_application.h | 18 - .../linux/runner/CMakeLists.txt | 26 - .../linux/runner/main.cc | 6 - .../linux/runner/my_application.cc | 130 -- .../linux/runner/my_application.h | 18 - .../macos/.gitignore | 7 - .../macos/Flutter/Flutter-Debug.xcconfig | 2 - .../macos/Flutter/Flutter-Release.xcconfig | 2 - .../Flutter/GeneratedPluginRegistrant.swift | 22 - .../macos/Podfile | 43 - .../macos/Podfile.lock | 83 -- .../macos/Runner.xcodeproj/project.pbxproj | 834 ------------- .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/xcschemes/Runner.xcscheme | 99 -- .../contents.xcworkspacedata | 10 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../macos/Runner/AppDelegate.swift | 13 - .../AppIcon.appiconset/Contents.json | 68 -- .../AppIcon.appiconset/app_icon_1024.png | Bin 102994 -> 0 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 5680 -> 0 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 520 -> 0 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 14142 -> 0 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 1066 -> 0 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 36406 -> 0 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 2218 -> 0 bytes .../macos/Runner/Base.lproj/MainMenu.xib | 343 ------ .../macos/Runner/Configs/AppInfo.xcconfig | 14 - .../macos/Runner/Configs/Debug.xcconfig | 2 - .../macos/Runner/Configs/Release.xcconfig | 2 - .../macos/Runner/Configs/Warnings.xcconfig | 13 - .../macos/Runner/DebugProfile.entitlements | 14 - .../macos/Runner/Info.plist | 32 - .../macos/Runner/MainFlutterWindow.swift | 15 - .../macos/Runner/Release.entitlements | 10 - .../macos/RunnerTests/RunnerTests.swift | 12 - .../pubspec.lock | 1061 ----------------- .../pubspec.yaml | 34 - .../web/favicon.png | Bin 917 -> 0 bytes .../web/icons/Icon-192.png | Bin 5292 -> 0 bytes .../web/icons/Icon-512.png | Bin 8252 -> 0 bytes .../web/icons/Icon-maskable-192.png | Bin 5594 -> 0 bytes .../web/icons/Icon-maskable-512.png | Bin 20998 -> 0 bytes .../web/index.html | 59 - .../web/manifest.json | 35 - .../windows/.gitignore | 17 - .../windows/CMakeLists.txt | 101 -- .../windows/flutter/CMakeLists.txt | 104 -- .../flutter/generated_plugin_registrant.cc | 23 - .../flutter/generated_plugin_registrant.h | 15 - .../windows/flutter/generated_plugins.cmake | 27 - .../windows/runner/CMakeLists.txt | 40 - .../windows/runner/Runner.rc | 121 -- .../windows/runner/flutter_window.cpp | 66 - .../windows/runner/flutter_window.h | 33 - .../windows/runner/main.cpp | 43 - .../windows/runner/resource.h | 16 - .../windows/runner/resources/app_icon.ico | Bin 33772 -> 0 bytes .../windows/runner/runner.exe.manifest | 20 - .../windows/runner/utils.cpp | 64 - .../windows/runner/utils.h | 19 - .../windows/runner/win32_window.cpp | 288 ----- .../windows/runner/win32_window.h | 102 -- .../lib/attachments/photo_capture_widget.dart | 43 +- .../lib/attachments/photo_widget.dart | 2 +- .../lib/attachments/queue.dart | 133 +-- .../attachments/remote_storage_adapter.dart | 74 +- .../supabase-todolist/lib/models/schema.dart | 2 +- .../lib/widgets/todo_item_widget.dart | 8 +- demos/supabase-todolist/pubspec.lock | 11 +- demos/supabase-todolist/pubspec.yaml | 3 +- .../powersync_attachments_stream/README.md | 270 ++++- .../src/abstractions/attachment_context.dart | 2 +- .../src/abstractions/attachment_service.dart | 4 +- .../lib/src/abstractions/local_storage.dart | 2 +- .../lib/src/abstractions/remote_storage.dart | 2 +- .../lib/src/attachment.dart | 3 +- .../lib/src/attachment_queue_service.dart | 8 +- .../implementations/attachment_context.dart | 2 +- .../implementations/attachment_service.dart | 6 +- .../lib/src/storage/io_local_storage.dart | 4 +- .../lib/src/sync/syncing_service.dart | 10 +- 184 files changed, 421 insertions(+), 8630 deletions(-) delete mode 100644 demos/supabase-todolist-new-attachment/.gitignore delete mode 100644 demos/supabase-todolist-new-attachment/.metadata delete mode 100644 demos/supabase-todolist-new-attachment/LICENSE delete mode 100644 demos/supabase-todolist-new-attachment/README.md delete mode 100644 demos/supabase-todolist-new-attachment/analysis_options.yaml delete mode 100644 demos/supabase-todolist-new-attachment/android/.gitignore delete mode 100644 demos/supabase-todolist-new-attachment/android/app/build.gradle.kts delete mode 100644 demos/supabase-todolist-new-attachment/android/app/src/debug/AndroidManifest.xml delete mode 100644 demos/supabase-todolist-new-attachment/android/app/src/main/AndroidManifest.xml delete mode 100644 demos/supabase-todolist-new-attachment/android/app/src/main/kotlin/com/powersync/powersync_flutter_demo/MainActivity.kt delete mode 100644 demos/supabase-todolist-new-attachment/android/app/src/main/res/drawable-v21/launch_background.xml delete mode 100644 demos/supabase-todolist-new-attachment/android/app/src/main/res/drawable/launch_background.xml delete mode 100644 demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 demos/supabase-todolist-new-attachment/android/app/src/main/res/values-night/styles.xml delete mode 100644 demos/supabase-todolist-new-attachment/android/app/src/main/res/values/styles.xml delete mode 100644 demos/supabase-todolist-new-attachment/android/app/src/profile/AndroidManifest.xml delete mode 100644 demos/supabase-todolist-new-attachment/android/build.gradle.kts delete mode 100644 demos/supabase-todolist-new-attachment/android/gradle.properties delete mode 100644 demos/supabase-todolist-new-attachment/android/gradle/wrapper/gradle-wrapper.properties delete mode 100644 demos/supabase-todolist-new-attachment/android/settings.gradle.kts delete mode 100644 demos/supabase-todolist-new-attachment/database.sql delete mode 100644 demos/supabase-todolist-new-attachment/flutter_01.png delete mode 100644 demos/supabase-todolist-new-attachment/ios/.gitignore delete mode 100644 demos/supabase-todolist-new-attachment/ios/Flutter/AppFrameworkInfo.plist delete mode 100644 demos/supabase-todolist-new-attachment/ios/Flutter/Debug.xcconfig delete mode 100644 demos/supabase-todolist-new-attachment/ios/Flutter/Release.xcconfig delete mode 100644 demos/supabase-todolist-new-attachment/ios/Podfile delete mode 100644 demos/supabase-todolist-new-attachment/ios/Podfile.lock delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.pbxproj delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner.xcworkspace/contents.xcworkspacedata delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/AppDelegate.swift delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Base.lproj/LaunchScreen.storyboard delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Base.lproj/Main.storyboard delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Info.plist delete mode 100644 demos/supabase-todolist-new-attachment/ios/Runner/Runner-Bridging-Header.h delete mode 100644 demos/supabase-todolist-new-attachment/ios/RunnerTests/RunnerTests.swift delete mode 100644 demos/supabase-todolist-new-attachment/lib/app_config_template.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/attachments/camera_helpers.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/attachments/photo_capture_widget.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/attachments/photo_widget.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/attachments/queue.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/attachments/remote_storage_adapter.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/fts_helpers.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/main.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/migrations/fts_setup.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/migrations/helpers.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/models/schema.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/models/todo_item.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/models/todo_list.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/powersync.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/supabase.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/fts_search_delegate.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/guard_by_sync.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/list_item.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/list_item_dialog.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/lists_page.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/login_page.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/query_widget.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/resultset_table.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/signup_page.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/status_app_bar.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/todo_item_dialog.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/todo_item_widget.dart delete mode 100644 demos/supabase-todolist-new-attachment/lib/widgets/todo_list_page.dart delete mode 100644 demos/supabase-todolist-new-attachment/linux/.gitignore delete mode 100644 demos/supabase-todolist-new-attachment/linux/CMakeLists.txt delete mode 100644 demos/supabase-todolist-new-attachment/linux/flutter/CMakeLists.txt delete mode 100644 demos/supabase-todolist-new-attachment/linux/flutter/generated_plugin_registrant.cc delete mode 100644 demos/supabase-todolist-new-attachment/linux/flutter/generated_plugin_registrant.h delete mode 100644 demos/supabase-todolist-new-attachment/linux/flutter/generated_plugins.cmake delete mode 100644 demos/supabase-todolist-new-attachment/linux/main.cc delete mode 100644 demos/supabase-todolist-new-attachment/linux/my_application.cc delete mode 100644 demos/supabase-todolist-new-attachment/linux/my_application.h delete mode 100644 demos/supabase-todolist-new-attachment/linux/runner/CMakeLists.txt delete mode 100644 demos/supabase-todolist-new-attachment/linux/runner/main.cc delete mode 100644 demos/supabase-todolist-new-attachment/linux/runner/my_application.cc delete mode 100644 demos/supabase-todolist-new-attachment/linux/runner/my_application.h delete mode 100644 demos/supabase-todolist-new-attachment/macos/.gitignore delete mode 100644 demos/supabase-todolist-new-attachment/macos/Flutter/Flutter-Debug.xcconfig delete mode 100644 demos/supabase-todolist-new-attachment/macos/Flutter/Flutter-Release.xcconfig delete mode 100644 demos/supabase-todolist-new-attachment/macos/Flutter/GeneratedPluginRegistrant.swift delete mode 100644 demos/supabase-todolist-new-attachment/macos/Podfile delete mode 100644 demos/supabase-todolist-new-attachment/macos/Podfile.lock delete mode 100644 demos/supabase-todolist-new-attachment/macos/Runner.xcodeproj/project.pbxproj delete mode 100644 demos/supabase-todolist-new-attachment/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 demos/supabase-todolist-new-attachment/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme delete mode 100644 demos/supabase-todolist-new-attachment/macos/Runner.xcworkspace/contents.xcworkspacedata delete mode 100644 demos/supabase-todolist-new-attachment/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/AppDelegate.swift delete mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png delete mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png delete mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png delete mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png delete mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png delete mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png delete mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png delete mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Base.lproj/MainMenu.xib delete mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Configs/AppInfo.xcconfig delete mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Configs/Debug.xcconfig delete mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Configs/Release.xcconfig delete mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Configs/Warnings.xcconfig delete mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/DebugProfile.entitlements delete mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Info.plist delete mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/MainFlutterWindow.swift delete mode 100644 demos/supabase-todolist-new-attachment/macos/Runner/Release.entitlements delete mode 100644 demos/supabase-todolist-new-attachment/macos/RunnerTests/RunnerTests.swift delete mode 100644 demos/supabase-todolist-new-attachment/pubspec.lock delete mode 100644 demos/supabase-todolist-new-attachment/pubspec.yaml delete mode 100644 demos/supabase-todolist-new-attachment/web/favicon.png delete mode 100644 demos/supabase-todolist-new-attachment/web/icons/Icon-192.png delete mode 100644 demos/supabase-todolist-new-attachment/web/icons/Icon-512.png delete mode 100644 demos/supabase-todolist-new-attachment/web/icons/Icon-maskable-192.png delete mode 100644 demos/supabase-todolist-new-attachment/web/icons/Icon-maskable-512.png delete mode 100644 demos/supabase-todolist-new-attachment/web/index.html delete mode 100644 demos/supabase-todolist-new-attachment/web/manifest.json delete mode 100644 demos/supabase-todolist-new-attachment/windows/.gitignore delete mode 100644 demos/supabase-todolist-new-attachment/windows/CMakeLists.txt delete mode 100644 demos/supabase-todolist-new-attachment/windows/flutter/CMakeLists.txt delete mode 100644 demos/supabase-todolist-new-attachment/windows/flutter/generated_plugin_registrant.cc delete mode 100644 demos/supabase-todolist-new-attachment/windows/flutter/generated_plugin_registrant.h delete mode 100644 demos/supabase-todolist-new-attachment/windows/flutter/generated_plugins.cmake delete mode 100644 demos/supabase-todolist-new-attachment/windows/runner/CMakeLists.txt delete mode 100644 demos/supabase-todolist-new-attachment/windows/runner/Runner.rc delete mode 100644 demos/supabase-todolist-new-attachment/windows/runner/flutter_window.cpp delete mode 100644 demos/supabase-todolist-new-attachment/windows/runner/flutter_window.h delete mode 100644 demos/supabase-todolist-new-attachment/windows/runner/main.cpp delete mode 100644 demos/supabase-todolist-new-attachment/windows/runner/resource.h delete mode 100644 demos/supabase-todolist-new-attachment/windows/runner/resources/app_icon.ico delete mode 100644 demos/supabase-todolist-new-attachment/windows/runner/runner.exe.manifest delete mode 100644 demos/supabase-todolist-new-attachment/windows/runner/utils.cpp delete mode 100644 demos/supabase-todolist-new-attachment/windows/runner/utils.h delete mode 100644 demos/supabase-todolist-new-attachment/windows/runner/win32_window.cpp delete mode 100644 demos/supabase-todolist-new-attachment/windows/runner/win32_window.h diff --git a/demos/supabase-todolist-new-attachment/.gitignore b/demos/supabase-todolist-new-attachment/.gitignore deleted file mode 100644 index 0f3655d3..00000000 --- a/demos/supabase-todolist-new-attachment/.gitignore +++ /dev/null @@ -1,52 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.build/ -.buildlog/ -.history -.svn/ -.swiftpm/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -**/ios/Flutter/.last_build_id -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.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 \ No newline at end of file diff --git a/demos/supabase-todolist-new-attachment/.metadata b/demos/supabase-todolist-new-attachment/.metadata deleted file mode 100644 index 6a623a4e..00000000 --- a/demos/supabase-todolist-new-attachment/.metadata +++ /dev/null @@ -1,45 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: "d7b523b356d15fb81e7d340bbe52b47f93937323" - channel: "stable" - -project_type: app - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 - base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 - - platform: android - create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 - base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 - - platform: ios - create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 - base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 - - platform: linux - create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 - base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 - - platform: macos - create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 - base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 - - platform: web - create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 - base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 - - platform: windows - create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 - base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 - - # User provided section - - # List of Local paths (relative to this file) that should be - # ignored by the migrate tool. - # - # Files that are not part of the templates will be ignored by default. - unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/demos/supabase-todolist-new-attachment/LICENSE b/demos/supabase-todolist-new-attachment/LICENSE deleted file mode 100644 index 0e259d42..00000000 --- a/demos/supabase-todolist-new-attachment/LICENSE +++ /dev/null @@ -1,121 +0,0 @@ -Creative Commons Legal Code - -CC0 1.0 Universal - - CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE - LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN - ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS - INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES - REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS - PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM - THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED - HEREUNDER. - -Statement of Purpose - -The laws of most jurisdictions throughout the world automatically confer -exclusive Copyright and Related Rights (defined below) upon the creator -and subsequent owner(s) (each and all, an "owner") of an original work of -authorship and/or a database (each, a "Work"). - -Certain owners wish to permanently relinquish those rights to a Work for -the purpose of contributing to a commons of creative, cultural and -scientific works ("Commons") that the public can reliably and without fear -of later claims of infringement build upon, modify, incorporate in other -works, reuse and redistribute as freely as possible in any form whatsoever -and for any purposes, including without limitation commercial purposes. -These owners may contribute to the Commons to promote the ideal of a free -culture and the further production of creative, cultural and scientific -works, or to gain reputation or greater distribution for their Work in -part through the use and efforts of others. - -For these and/or other purposes and motivations, and without any -expectation of additional consideration or compensation, the person -associating CC0 with a Work (the "Affirmer"), to the extent that he or she -is an owner of Copyright and Related Rights in the Work, voluntarily -elects to apply CC0 to the Work and publicly distribute the Work under its -terms, with knowledge of his or her Copyright and Related Rights in the -Work and the meaning and intended legal effect of CC0 on those rights. - -1. Copyright and Related Rights. A Work made available under CC0 may be -protected by copyright and related or neighboring rights ("Copyright and -Related Rights"). Copyright and Related Rights include, but are not -limited to, the following: - - i. the right to reproduce, adapt, distribute, perform, display, - communicate, and translate a Work; - ii. moral rights retained by the original author(s) and/or performer(s); -iii. publicity and privacy rights pertaining to a person's image or - likeness depicted in a Work; - iv. rights protecting against unfair competition in regards to a Work, - subject to the limitations in paragraph 4(a), below; - v. rights protecting the extraction, dissemination, use and reuse of data - in a Work; - vi. database rights (such as those arising under Directive 96/9/EC of the - European Parliament and of the Council of 11 March 1996 on the legal - protection of databases, and under any national implementation - thereof, including any amended or successor version of such - directive); and -vii. other similar, equivalent or corresponding rights throughout the - world based on applicable law or treaty, and any national - implementations thereof. - -2. Waiver. To the greatest extent permitted by, but not in contravention -of, applicable law, Affirmer hereby overtly, fully, permanently, -irrevocably and unconditionally waives, abandons, and surrenders all of -Affirmer's Copyright and Related Rights and associated claims and causes -of action, whether now known or unknown (including existing as well as -future claims and causes of action), in the Work (i) in all territories -worldwide, (ii) for the maximum duration provided by applicable law or -treaty (including future time extensions), (iii) in any current or future -medium and for any number of copies, and (iv) for any purpose whatsoever, -including without limitation commercial, advertising or promotional -purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each -member of the public at large and to the detriment of Affirmer's heirs and -successors, fully intending that such Waiver shall not be subject to -revocation, rescission, cancellation, termination, or any other legal or -equitable action to disrupt the quiet enjoyment of the Work by the public -as contemplated by Affirmer's express Statement of Purpose. - -3. Public License Fallback. Should any part of the Waiver for any reason -be judged legally invalid or ineffective under applicable law, then the -Waiver shall be preserved to the maximum extent permitted taking into -account Affirmer's express Statement of Purpose. In addition, to the -extent the Waiver is so judged Affirmer hereby grants to each affected -person a royalty-free, non transferable, non sublicensable, non exclusive, -irrevocable and unconditional license to exercise Affirmer's Copyright and -Related Rights in the Work (i) in all territories worldwide, (ii) for the -maximum duration provided by applicable law or treaty (including future -time extensions), (iii) in any current or future medium and for any number -of copies, and (iv) for any purpose whatsoever, including without -limitation commercial, advertising or promotional purposes (the -"License"). The License shall be deemed effective as of the date CC0 was -applied by Affirmer to the Work. Should any part of the License for any -reason be judged legally invalid or ineffective under applicable law, such -partial invalidity or ineffectiveness shall not invalidate the remainder -of the License, and in such case Affirmer hereby affirms that he or she -will not (i) exercise any of his or her remaining Copyright and Related -Rights in the Work or (ii) assert any associated claims and causes of -action with respect to the Work, in either case contrary to Affirmer's -express Statement of Purpose. - -4. Limitations and Disclaimers. - - a. No trademark or patent rights held by Affirmer are waived, abandoned, - surrendered, licensed or otherwise affected by this document. - b. Affirmer offers the Work as-is and makes no representations or - warranties of any kind concerning the Work, express, implied, - statutory or otherwise, including without limitation warranties of - title, merchantability, fitness for a particular purpose, non - infringement, or the absence of latent or other defects, accuracy, or - the present or absence of errors, whether or not discoverable, all to - the greatest extent permissible under applicable law. - c. Affirmer disclaims responsibility for clearing rights of other persons - that may apply to the Work or any use thereof, including without - limitation any person's Copyright and Related Rights in the Work. - Further, Affirmer disclaims responsibility for obtaining any necessary - consents, permissions or other rights required for any use of the - Work. - d. Affirmer understands and acknowledges that Creative Commons is not a - party to this document and has no duty or obligation with respect to - this CC0 or use of the Work. diff --git a/demos/supabase-todolist-new-attachment/README.md b/demos/supabase-todolist-new-attachment/README.md deleted file mode 100644 index 55241dde..00000000 --- a/demos/supabase-todolist-new-attachment/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# PowerSync + Supabase Flutter Demo: Todo List App - -Demo app demonstrating use of the PowerSync SDK for Flutter together with Supabase. For a step-by-step guide, see [here](https://docs.powersync.com/integration-guides/supabase). - -# Running the app - -Ensure you have [melos](https://melos.invertase.dev/~melos-latest/getting-started) installed. - -1. `cd demos/supabase-todolist` -2. `melos prepare` -3. `cp lib/app_config_template.dart lib/app_config.dart` -4. Insert your Supabase and PowerSync project credentials into `lib/app_config.dart` (See instructions below) -5. `flutter run` - -# Set up Supabase Project - -Create a new Supabase project, and paste an run the contents of [database.sql](./database.sql) in the Supabase SQL editor. - -It does the following: - -1. Create `lists` and `todos` tables. -2. Create a publication called `powersync` for `lists` and `todos`. -3. Enable row level security, allowing users to only view and edit their own data. -4. Create a trigger to populate some sample data when an user registers. - -# Set up PowerSync Instance - -Create a new PowerSync instance, connecting to the database of the Supabase project. - -Then deploy the following sync rules: - -```yaml -bucket_definitions: - user_lists: - priority: 1 - parameters: select id as list_id from lists where owner_id = request.user_id() - data: - - select * from lists where id = bucket.list_id - - user_todos: - parameters: select id as list_id from lists where owner_id = request.user_id() - data: - - select * from todos where list_id = bucket.list_id -``` - -**Note**: These rules showcase [prioritized sync](https://docs.powersync.com/usage/use-case-examples/prioritized-sync), -by syncing a user's lists with a higher priority than the items within a list (todos). This can be -useful to keep the list overview page reactive during a large sync cycle affecting many -rows in the `user_todos` bucket. The two buckets can also be unified into a single one if -priorities are not important (the app will work without changes): - -```yaml -bucket_definitions: - user_lists: - # Separate bucket per todo list - parameters: select id as list_id from lists where owner_id = request.user_id() - data: - - select * from lists where id = bucket.list_id - - select * from todos where list_id = bucket.list_id -``` - -# Configure the app - -Insert the credentials of your new Supabase and PowerSync projects into `lib/app_config.dart` diff --git a/demos/supabase-todolist-new-attachment/analysis_options.yaml b/demos/supabase-todolist-new-attachment/analysis_options.yaml deleted file mode 100644 index 61b6c4de..00000000 --- a/demos/supabase-todolist-new-attachment/analysis_options.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml - -linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at - # https://dart-lang.github.io/linter/lints/index.html. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. - rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options diff --git a/demos/supabase-todolist-new-attachment/android/.gitignore b/demos/supabase-todolist-new-attachment/android/.gitignore deleted file mode 100644 index be3943c9..00000000 --- a/demos/supabase-todolist-new-attachment/android/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -gradle-wrapper.jar -/.gradle -/captures/ -/gradlew -/gradlew.bat -/local.properties -GeneratedPluginRegistrant.java -.cxx/ - -# Remember to never publicly share your keystore. -# See https://flutter.dev/to/reference-keystore -key.properties -**/*.keystore -**/*.jks diff --git a/demos/supabase-todolist-new-attachment/android/app/build.gradle.kts b/demos/supabase-todolist-new-attachment/android/app/build.gradle.kts deleted file mode 100644 index 512cc9e7..00000000 --- a/demos/supabase-todolist-new-attachment/android/app/build.gradle.kts +++ /dev/null @@ -1,44 +0,0 @@ -plugins { - id("com.android.application") - id("kotlin-android") - // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. - id("dev.flutter.flutter-gradle-plugin") -} - -android { - namespace = "com.powersync.powersync_flutter_demo" - compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "com.powersync.powersync_flutter_demo" - // You can update the following values to match your application needs. - // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = flutter.minSdkVersion - targetSdk = flutter.targetSdkVersion - versionCode = flutter.versionCode - versionName = flutter.versionName - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig = signingConfigs.getByName("debug") - } - } -} - -flutter { - source = "../.." -} diff --git a/demos/supabase-todolist-new-attachment/android/app/src/debug/AndroidManifest.xml b/demos/supabase-todolist-new-attachment/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 399f6981..00000000 --- a/demos/supabase-todolist-new-attachment/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/demos/supabase-todolist-new-attachment/android/app/src/main/AndroidManifest.xml b/demos/supabase-todolist-new-attachment/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index e85f03e3..00000000 --- a/demos/supabase-todolist-new-attachment/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/demos/supabase-todolist-new-attachment/android/app/src/main/kotlin/com/powersync/powersync_flutter_demo/MainActivity.kt b/demos/supabase-todolist-new-attachment/android/app/src/main/kotlin/com/powersync/powersync_flutter_demo/MainActivity.kt deleted file mode 100644 index 46cf42a6..00000000 --- a/demos/supabase-todolist-new-attachment/android/app/src/main/kotlin/com/powersync/powersync_flutter_demo/MainActivity.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.powersync.powersync_flutter_demo - -import io.flutter.embedding.android.FlutterActivity - -class MainActivity : FlutterActivity() diff --git a/demos/supabase-todolist-new-attachment/android/app/src/main/res/drawable-v21/launch_background.xml b/demos/supabase-todolist-new-attachment/android/app/src/main/res/drawable-v21/launch_background.xml deleted file mode 100644 index f74085f3..00000000 --- a/demos/supabase-todolist-new-attachment/android/app/src/main/res/drawable-v21/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/demos/supabase-todolist-new-attachment/android/app/src/main/res/drawable/launch_background.xml b/demos/supabase-todolist-new-attachment/android/app/src/main/res/drawable/launch_background.xml deleted file mode 100644 index 304732f8..00000000 --- a/demos/supabase-todolist-new-attachment/android/app/src/main/res/drawable/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4b7b0906d62b1847e87f15cdcacf6a4f29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ diff --git a/demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 17987b79bb8a35cc66c3c1fd44f5a5526c1b78be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ diff --git a/demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d34e7a88e3f88bea192c3a370d44689c3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof diff --git a/demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/supabase-todolist-new-attachment/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372eebdb28e45604e46eeda8dd24651419bc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` diff --git a/demos/supabase-todolist-new-attachment/android/app/src/main/res/values-night/styles.xml b/demos/supabase-todolist-new-attachment/android/app/src/main/res/values-night/styles.xml deleted file mode 100644 index 06952be7..00000000 --- a/demos/supabase-todolist-new-attachment/android/app/src/main/res/values-night/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/demos/supabase-todolist-new-attachment/android/app/src/main/res/values/styles.xml b/demos/supabase-todolist-new-attachment/android/app/src/main/res/values/styles.xml deleted file mode 100644 index cb1ef880..00000000 --- a/demos/supabase-todolist-new-attachment/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/demos/supabase-todolist-new-attachment/android/app/src/profile/AndroidManifest.xml b/demos/supabase-todolist-new-attachment/android/app/src/profile/AndroidManifest.xml deleted file mode 100644 index 399f6981..00000000 --- a/demos/supabase-todolist-new-attachment/android/app/src/profile/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/demos/supabase-todolist-new-attachment/android/build.gradle.kts b/demos/supabase-todolist-new-attachment/android/build.gradle.kts deleted file mode 100644 index 89176ef4..00000000 --- a/demos/supabase-todolist-new-attachment/android/build.gradle.kts +++ /dev/null @@ -1,21 +0,0 @@ -allprojects { - repositories { - google() - mavenCentral() - } -} - -val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() -rootProject.layout.buildDirectory.value(newBuildDir) - -subprojects { - val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) - project.layout.buildDirectory.value(newSubprojectBuildDir) -} -subprojects { - project.evaluationDependsOn(":app") -} - -tasks.register("clean") { - delete(rootProject.layout.buildDirectory) -} diff --git a/demos/supabase-todolist-new-attachment/android/gradle.properties b/demos/supabase-todolist-new-attachment/android/gradle.properties deleted file mode 100644 index f018a618..00000000 --- a/demos/supabase-todolist-new-attachment/android/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError -android.useAndroidX=true -android.enableJetifier=true diff --git a/demos/supabase-todolist-new-attachment/android/gradle/wrapper/gradle-wrapper.properties b/demos/supabase-todolist-new-attachment/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index ac3b4792..00000000 --- a/demos/supabase-todolist-new-attachment/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/demos/supabase-todolist-new-attachment/android/settings.gradle.kts b/demos/supabase-todolist-new-attachment/android/settings.gradle.kts deleted file mode 100644 index ab39a10a..00000000 --- a/demos/supabase-todolist-new-attachment/android/settings.gradle.kts +++ /dev/null @@ -1,25 +0,0 @@ -pluginManagement { - val flutterSdkPath = run { - val properties = java.util.Properties() - file("local.properties").inputStream().use { properties.load(it) } - val flutterSdkPath = properties.getProperty("flutter.sdk") - require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } - flutterSdkPath - } - - includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} - -plugins { - id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.7.3" apply false - id("org.jetbrains.kotlin.android") version "2.1.0" apply false -} - -include(":app") diff --git a/demos/supabase-todolist-new-attachment/database.sql b/demos/supabase-todolist-new-attachment/database.sql deleted file mode 100644 index eecc9768..00000000 --- a/demos/supabase-todolist-new-attachment/database.sql +++ /dev/null @@ -1,71 +0,0 @@ --- Create tables -create table - public.lists ( - id uuid not null default gen_random_uuid (), - created_at timestamp with time zone not null default now(), - name text not null, - owner_id uuid not null, - constraint lists_pkey primary key (id), - constraint lists_owner_id_fkey foreign key (owner_id) references auth.users (id) on delete cascade - ) tablespace pg_default; - -create table - public.todos ( - id uuid not null default gen_random_uuid (), - created_at timestamp with time zone not null default now(), - completed_at timestamp with time zone null, - description text not null, - completed boolean not null default false, - created_by uuid null, - completed_by uuid null, - list_id uuid not null, - photo_id uuid null, - constraint todos_pkey primary key (id), - constraint todos_created_by_fkey foreign key (created_by) references auth.users (id) on delete set null, - constraint todos_completed_by_fkey foreign key (completed_by) references auth.users (id) on delete set null, - constraint todos_list_id_fkey foreign key (list_id) references lists (id) on delete cascade - ) tablespace pg_default; - --- Create publication for powersync -create publication powersync for table lists, todos; - --- Set up Row Level Security (RLS) --- See https://supabase.com/docs/guides/auth/row-level-security for more details. -alter table public.lists - enable row level security; - -alter table public.todos - enable row level security; - -create policy "owned lists" on public.lists for ALL using ( - auth.uid() = owner_id -); - -create policy "todos in owned lists" on public.todos for ALL using ( - auth.uid() IN ( - SELECT lists.owner_id FROM lists WHERE (lists.id = todos.list_id) - ) -); - --- This trigger automatically creates some sample data when a user registers. --- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details. -create function public.handle_new_user_sample_data() -returns trigger as $$ -declare - new_list_id uuid; -begin - insert into public.lists (name, owner_id) - values ('Shopping list', new.id) - returning id into new_list_id; - - insert into public.todos(description, list_id, created_by) - values ('Bread', new_list_id, new.id); - - insert into public.todos(description, list_id, created_by) - values ('Apples', new_list_id, new.id); - - return new; -end; -$$ language plpgsql security definer; - -create trigger new_user_sample_data after insert on auth.users for each row execute procedure public.handle_new_user_sample_data(); diff --git a/demos/supabase-todolist-new-attachment/flutter_01.png b/demos/supabase-todolist-new-attachment/flutter_01.png deleted file mode 100644 index 595f48580be688d7657ff95f92e4f05172ff3eac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 73026 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 diff --git a/demos/supabase-todolist-new-attachment/ios/.gitignore b/demos/supabase-todolist-new-attachment/ios/.gitignore deleted file mode 100644 index 7a7f9873..00000000 --- a/demos/supabase-todolist-new-attachment/ios/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -**/dgph -*.mode1v3 -*.mode2v3 -*.moved-aside -*.pbxuser -*.perspectivev3 -**/*sync/ -.sconsign.dblite -.tags* -**/.vagrant/ -**/DerivedData/ -Icon? -**/Pods/ -**/.symlinks/ -profile -xcuserdata -**/.generated/ -Flutter/App.framework -Flutter/Flutter.framework -Flutter/Flutter.podspec -Flutter/Generated.xcconfig -Flutter/ephemeral/ -Flutter/app.flx -Flutter/app.zip -Flutter/flutter_assets/ -Flutter/flutter_export_environment.sh -ServiceDefinitions.json -Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!default.mode1v3 -!default.mode2v3 -!default.pbxuser -!default.perspectivev3 diff --git a/demos/supabase-todolist-new-attachment/ios/Flutter/AppFrameworkInfo.plist b/demos/supabase-todolist-new-attachment/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 7c569640..00000000 --- a/demos/supabase-todolist-new-attachment/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 12.0 - - diff --git a/demos/supabase-todolist-new-attachment/ios/Flutter/Debug.xcconfig b/demos/supabase-todolist-new-attachment/ios/Flutter/Debug.xcconfig deleted file mode 100644 index ec97fc6f..00000000 --- a/demos/supabase-todolist-new-attachment/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/demos/supabase-todolist-new-attachment/ios/Flutter/Release.xcconfig b/demos/supabase-todolist-new-attachment/ios/Flutter/Release.xcconfig deleted file mode 100644 index c4855bfe..00000000 --- a/demos/supabase-todolist-new-attachment/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/demos/supabase-todolist-new-attachment/ios/Podfile b/demos/supabase-todolist-new-attachment/ios/Podfile deleted file mode 100644 index e9f73048..00000000 --- a/demos/supabase-todolist-new-attachment/ios/Podfile +++ /dev/null @@ -1,48 +0,0 @@ -# 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/supabase-todolist-new-attachment/ios/Podfile.lock b/demos/supabase-todolist-new-attachment/ios/Podfile.lock deleted file mode 100644 index aac7e03f..00000000 --- a/demos/supabase-todolist-new-attachment/ios/Podfile.lock +++ /dev/null @@ -1,89 +0,0 @@ -PODS: - - app_links (0.0.2): - - Flutter - - camera_avfoundation (0.0.1): - - Flutter - - Flutter (1.0.0) - - path_provider_foundation (0.0.1): - - Flutter - - FlutterMacOS - - powersync-sqlite-core (0.4.2) - - powersync_flutter_libs (0.0.1): - - Flutter - - powersync-sqlite-core (~> 0.4.2) - - shared_preferences_foundation (0.0.1): - - Flutter - - FlutterMacOS - - sqlite3 (3.49.2): - - sqlite3/common (= 3.49.2) - - sqlite3/common (3.49.2) - - sqlite3/dbstatvtab (3.49.2): - - sqlite3/common - - sqlite3/fts5 (3.49.2): - - sqlite3/common - - sqlite3/math (3.49.2): - - sqlite3/common - - sqlite3/perf-threadsafe (3.49.2): - - sqlite3/common - - sqlite3/rtree (3.49.2): - - sqlite3/common - - sqlite3_flutter_libs (0.0.1): - - Flutter - - FlutterMacOS - - sqlite3 (~> 3.49.1) - - sqlite3/dbstatvtab - - sqlite3/fts5 - - sqlite3/math - - sqlite3/perf-threadsafe - - sqlite3/rtree - - url_launcher_ios (0.0.1): - - Flutter - -DEPENDENCIES: - - app_links (from `.symlinks/plugins/app_links/ios`) - - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) - - Flutter (from `Flutter`) - - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - - powersync_flutter_libs (from `.symlinks/plugins/powersync_flutter_libs/ios`) - - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) - - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - -SPEC REPOS: - trunk: - - powersync-sqlite-core - - sqlite3 - -EXTERNAL SOURCES: - app_links: - :path: ".symlinks/plugins/app_links/ios" - camera_avfoundation: - :path: ".symlinks/plugins/camera_avfoundation/ios" - Flutter: - :path: Flutter - path_provider_foundation: - :path: ".symlinks/plugins/path_provider_foundation/darwin" - powersync_flutter_libs: - :path: ".symlinks/plugins/powersync_flutter_libs/ios" - shared_preferences_foundation: - :path: ".symlinks/plugins/shared_preferences_foundation/darwin" - sqlite3_flutter_libs: - :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" - url_launcher_ios: - :path: ".symlinks/plugins/url_launcher_ios/ios" - -SPEC CHECKSUMS: - app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7 - camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436 - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 - powersync-sqlite-core: a58efd88833861f0a8bb636c171bdf0ed55c9801 - powersync_flutter_libs: 881187a07f70ecabaf802fce45b186485464d618 - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 - sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d - -PODFILE CHECKSUM: f7b3cb7384a2d5da4b22b090e1f632de7f377987 - -COCOAPODS: 1.16.2 diff --git a/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.pbxproj b/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index e32e9fa6..00000000 --- a/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,552 +0,0 @@ -// !$*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 */; }; - FE22D026B50D91C63EC1E548 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3246A2C54ACF47297A0D9A97 /* 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 = ""; }; - 1AC6D6834A180EC866A1A907 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 3246A2C54ACF47297A0D9A97 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7F544BD3701C5CF77F2FF87F /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 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 = ""; }; - F866E203CA1A8E0C6D9ABA5C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - FE22D026B50D91C63EC1E548 /* Pods_Runner.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 114FDDDC75E03531AE956759 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 3246A2C54ACF47297A0D9A97 /* Pods_Runner.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 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 */, - 114FDDDC75E03531AE956759 /* 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 = ( - F866E203CA1A8E0C6D9ABA5C /* Pods-Runner.debug.xcconfig */, - 1AC6D6834A180EC866A1A907 /* Pods-Runner.release.xcconfig */, - 7F544BD3701C5CF77F2FF87F /* Pods-Runner.profile.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 39B3199DCFC6D38A9384399C /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - FE8EB41334261949D37FC328 /* [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 */ - 39B3199DCFC6D38A9384399C /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - 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"; - }; - FE8EB41334261949D37FC328 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 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/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a6..00000000 --- a/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c5..00000000 --- a/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 9c12df59..00000000 --- a/demos/supabase-todolist-new-attachment/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/demos/supabase-todolist-new-attachment/ios/Runner.xcworkspace/contents.xcworkspacedata b/demos/supabase-todolist-new-attachment/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14..00000000 --- a/demos/supabase-todolist-new-attachment/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/demos/supabase-todolist-new-attachment/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demos/supabase-todolist-new-attachment/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/demos/supabase-todolist-new-attachment/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/demos/supabase-todolist-new-attachment/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/demos/supabase-todolist-new-attachment/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c5..00000000 --- a/demos/supabase-todolist-new-attachment/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/AppDelegate.swift b/demos/supabase-todolist-new-attachment/ios/Runner/AppDelegate.swift deleted file mode 100644 index b6363034..00000000 --- a/demos/supabase-todolist-new-attachment/ios/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -import UIKit -import Flutter - -@main -@objc class AppDelegate: FlutterAppDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } -} diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d36b1fab..00000000 --- a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index dc9ada4725e9b0ddb1deab583e5b5102493aa332..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10932 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 diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 797d452e458972bab9d994556c8305db4c827017..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 406 zcmV;H0crk;P))>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index 6ed2d933e1120817fe9182483a228007b18ab6ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 450 zcmV;z0X_bSP)iGWQ_5NJQ_~rNh*z)}eT%KUb z`7gNk0#AwF^#0T0?hIa^`~Ck;!}#m+_uT050aTR(J!bU#|IzRL%^UsMS#KsYnTF*!YeDOytlP4VhV?b} z%rz_<=#CPc)tU1MZTq~*2=8~iZ!lSa<{9b@2Jl;?IEV8)=fG217*|@)CCYgFze-x? zIFODUIA>nWKpE+bn~n7;-89sa>#DR>TSlqWk*!2hSN6D~Qb#VqbP~4Fk&m`@1$JGr zXPIdeRE&b2Thd#{MtDK$px*d3-Wx``>!oimf%|A-&-q*6KAH)e$3|6JV%HX{Hig)k suLT-RhftRq8b9;(V=235Wa|I=027H2wCDra;{X5v07*qoM6N<$f;9x^2LJ#7 diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cd7b0099ca80c806f8fe495613e8d6c69460d76..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 282 zcmV+#0p(^bcu7P-R4C8Q z&e;xxFbF_Vrezo%_kH*OKhshZ6BFpG-Y1e10`QXJKbND7AMQ&cMj60B5TNObaZxYybcN07*qoM6N<$g3m;S%K!iX diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index fe730945a01f64a61e2235dbe3f45b08f7729182..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 462 zcmV;<0WtoGP)-}iV`2<;=$?g5M=KQbZ{F&YRNy7Nn@%_*5{gvDM0aKI4?ESmw z{NnZg)A0R`+4?NF_RZexyVB&^^ZvN!{I28tr{Vje;QNTz`dG&Jz0~Ek&f2;*Z7>B|cg}xYpxEFY+0YrKLF;^Q+-HreN0P{&i zK~zY`?b7ECf-n?@;d<&orQ*Q7KoR%4|C>{W^h6@&01>0SKS`dn{Q}GT%Qj_{PLZ_& zs`MFI#j-(>?bvdZ!8^xTwlY{qA)T4QLbY@j(!YJ7aXJervHy6HaG_2SB`6CC{He}f zHVw(fJWApwPq!6VY7r1w-Fs)@ox~N+q|w~e;JI~C4Vf^@d>Wvj=fl`^u9x9wd9 zR%3*Q+)t%S!MU_`id^@&Y{y7-r98lZX0?YrHlfmwb?#}^1b{8g&KzmkE(L>Z&)179 zp<)v6Y}pRl100G2FL_t(o!|l{-Q-VMg#&MKg7c{O0 z2wJImOS3Gy*Z2Qifdv~JYOp;v+U)a|nLoc7hNH;I$;lzDt$}rkaFw1mYK5_0Q(Sut zvbEloxON7$+HSOgC9Z8ltuC&0OSF!-mXv5caV>#bc3@hBPX@I$58-z}(ZZE!t-aOG zpjNkbau@>yEzH(5Yj4kZiMH32XI!4~gVXNnjAvRx;Sdg^`>2DpUEwoMhTs_st8pKG z(%SHyHdU&v%f36~uERh!bd`!T2dw;z6PrOTQ7Vt*#9F2uHlUVnb#ev_o^fh}Dzmq} zWtlk35}k=?xj28uO|5>>$yXadTUE@@IPpgH`gJ~Ro4>jd1IF|(+IX>8M4Ps{PNvmI zNj4D+XgN83gPt_Gm}`Ybv{;+&yu-C(Grdiahmo~BjG-l&mWM+{e5M1sm&=xduwgM9 z`8OEh`=F3r`^E{n_;%9weN{cf2%7=VzC@cYj+lg>+3|D|_1C@{hcU(DyQG_BvBWe? zvTv``=%b1zrol#=R`JB)>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index 502f463a9bc882b461c96aadf492d1729e49e725..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 586 zcmV-Q0=4~#P)+}#`wDE{8-2Mebf5<{{PqV{TgVcv*r8?UZ3{-|G?_}T*&y;@cqf{ z{Q*~+qr%%p!1pS*_Uicl#q9lc(D`!D`LN62sNwq{oYw(Wmhk)k<@f$!$@ng~_5)Ru z0Z)trIA5^j{DIW^c+vT2%lW+2<(RtE2wR;4O@)Tm`Xr*?A(qYoM}7i5Yxw>D(&6ou zxz!_Xr~yNF+waPe00049Nkl*;a!v6h%{rlvIH#gW3s8p;bFr=l}mRqpW2h zw=OA%hdyL~z+UHOzl0eKhEr$YYOL-c-%Y<)=j?(bzDweB7{b+%_ypvm_cG{SvM=DK zhv{K@m>#Bw>2W$eUI#iU)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G! zhkE!s;%oku3;IwG3U^2kw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn`0X*2 zy3(k600_CSZj?O$Qu%&$;|TGUJrptR(HzyIx>5E(2r{eA(<6t3e3I0B)7d6s7?Z5J zZ!rtKvA{MiEBm&KFtoifx>5P^Z=vl)95XJn()aS5%ad(s?4-=Tkis9IGu{`Fy8r+H07*qoM6N<$f20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index 0ec303439225b78712f49115768196d8d76f6790..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 862 zcmV-k1EKthP)20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index e9f5fea27c705180eb716271f41b582e76dcbd90..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1674 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 diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index 0467bf12aa4d28f374bb26596605a46dcbb3e7c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1418 zcmV;51$Fv~P)q zKfU)WzW*n(@|xWGCA9ScMt*e9`2kdxPQ&&>|-UCa7_51w+ zLUsW@ZzZSW0y$)Hp~e9%PvP|a03ks1`~K?q{u;6NC8*{AOqIUq{CL&;p56Lf$oQGq z^={4hPQv)y=I|4n+?>7Fim=dxt1 z2H+Dm+1+fh+IF>G0SjJMkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJT zkdTm&kdTm&kdTm&kdP`esgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>Tmf` zm1{eLgw!b{bXkjWbF%dTkTZEJWyWOb##Lfw4EK2}<0d6%>AGS{po>WCOy&f$Tay_> z?NBlkpo@s-O;0V%Y_Xa-G#_O08q5LR*~F%&)}{}r&L%Sbs8AS4t7Y0NEx*{soY=0MZExqA5XHQkqi#4gW3 zqODM^iyZl;dvf)-bOXtOru(s)Uc7~BFx{w-FK;2{`VA?(g&@3z&bfLFyctOH!cVsF z7IL=fo-qBndRUm;kAdXR4e6>k-z|21AaN%ubeVrHl*<|s&Ax@W-t?LR(P-24A5=>a z*R9#QvjzF8n%@1Nw@?CG@6(%>+-0ASK~jEmCV|&a*7-GKT72W<(TbSjf)&Eme6nGE z>Gkj4Sq&2e+-G%|+NM8OOm5zVl9{Z8Dd8A5z3y8mZ=4Bv4%>as_{9cN#bm~;h>62( zdqY93Zy}v&c4n($Vv!UybR8ocs7#zbfX1IY-*w~)p}XyZ-SFC~4w>BvMVr`dFbelV{lLL0bx7@*ZZdebr3`sP;? zVImji)kG)(6Juv0lz@q`F!k1FE;CQ(D0iG$wchPbKZQELlsZ#~rt8#90Y_Xh&3U-< z{s<&cCV_1`^TD^ia9!*mQDq& zn2{r`j};V|uV%_wsP!zB?m%;FeaRe+X47K0e+KE!8C{gAWF8)lCd1u1%~|M!XNRvw zvtqy3iz0WSpWdhn6$hP8PaRBmp)q`#PCA`Vd#Tc$@f1tAcM>f_I@bC)hkI9|o(Iqv zo}Piadq!j76}004RBio<`)70k^`K1NK)q>w?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF z34$0Z;QO!JOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUC YUoZo%k(ykuW&i*H07*qoM6N<$f+CH{y8r+H diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json deleted file mode 100644 index 0bedcf2f..00000000 --- a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "LaunchImage.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png deleted file mode 100644 index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100644 index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100644 index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725b..00000000 --- a/demos/supabase-todolist-new-attachment/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Base.lproj/LaunchScreen.storyboard b/demos/supabase-todolist-new-attachment/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f2e259c7..00000000 --- a/demos/supabase-todolist-new-attachment/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Base.lproj/Main.storyboard b/demos/supabase-todolist-new-attachment/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516..00000000 --- a/demos/supabase-todolist-new-attachment/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/demos/supabase-todolist-new-attachment/ios/Runner/Info.plist b/demos/supabase-todolist-new-attachment/ios/Runner/Info.plist deleted file mode 100644 index fc3bb480..00000000 --- a/demos/supabase-todolist-new-attachment/ios/Runner/Info.plist +++ /dev/null @@ -1,55 +0,0 @@ - - - - - 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/supabase-todolist-new-attachment/ios/Runner/Runner-Bridging-Header.h b/demos/supabase-todolist-new-attachment/ios/Runner/Runner-Bridging-Header.h deleted file mode 100644 index 308a2a56..00000000 --- a/demos/supabase-todolist-new-attachment/ios/Runner/Runner-Bridging-Header.h +++ /dev/null @@ -1 +0,0 @@ -#import "GeneratedPluginRegistrant.h" diff --git a/demos/supabase-todolist-new-attachment/ios/RunnerTests/RunnerTests.swift b/demos/supabase-todolist-new-attachment/ios/RunnerTests/RunnerTests.swift deleted file mode 100644 index 86a7c3b1..00000000 --- a/demos/supabase-todolist-new-attachment/ios/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Flutter -import UIKit -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/demos/supabase-todolist-new-attachment/lib/app_config_template.dart b/demos/supabase-todolist-new-attachment/lib/app_config_template.dart deleted file mode 100644 index ccfdfa21..00000000 --- a/demos/supabase-todolist-new-attachment/lib/app_config_template.dart +++ /dev/null @@ -1,9 +0,0 @@ -// Copy this template: `cp lib/app_config_template.dart lib/app_config.dart` -// Edit lib/app_config.dart and enter your Supabase and PowerSync project details. -class AppConfig { - static const String supabaseUrl = 'https://foo.supabase.co'; - static const String supabaseAnonKey = 'foo'; - static const String powersyncUrl = 'https://foo.powersync.journeyapps.com'; - static const String supabaseStorageBucket = - ''; // Optional. Only required when syncing attachments and using Supabase Storage. See packages/powersync_attachments_helper. -} diff --git a/demos/supabase-todolist-new-attachment/lib/attachments/camera_helpers.dart b/demos/supabase-todolist-new-attachment/lib/attachments/camera_helpers.dart deleted file mode 100644 index cf2a2cf5..00000000 --- a/demos/supabase-todolist-new-attachment/lib/attachments/camera_helpers.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:camera/camera.dart'; -import 'package:flutter/widgets.dart'; -import 'package:powersync_flutter_demo_new/powersync.dart'; - -Future setupCamera() async { - // Ensure that plugin services are initialized so that `availableCameras()` - // can be called before `runApp()` - WidgetsFlutterBinding.ensureInitialized(); - // Obtain a list of the available cameras on the device. - try { - final cameras = await availableCameras(); - // Get a specific camera from the list of available cameras. - final camera = cameras.isNotEmpty ? cameras.first : null; - return camera; - } catch (e) { - // Camera is not supported on all platforms - log.warning('Failed to setup camera: $e'); - return null; - } -} diff --git a/demos/supabase-todolist-new-attachment/lib/attachments/photo_capture_widget.dart b/demos/supabase-todolist-new-attachment/lib/attachments/photo_capture_widget.dart deleted file mode 100644 index 3b7ed33d..00000000 --- a/demos/supabase-todolist-new-attachment/lib/attachments/photo_capture_widget.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:typed_data'; -import 'package:camera/camera.dart'; -import 'package:flutter/material.dart'; -import 'package:logging/logging.dart'; -import 'package:powersync_flutter_demo_new/attachments/queue.dart'; - -class TakePhotoWidget extends StatefulWidget { - final String todoId; - final CameraDescription camera; - - const TakePhotoWidget({super.key, required this.todoId, required this.camera}); - - @override - State createState() { - return _TakePhotoWidgetState(); - } -} - -class _TakePhotoWidgetState extends State { - late CameraController _cameraController; - late Future _initializeControllerFuture; - final log = Logger('TakePhotoWidget'); - - @override - void initState() { - super.initState(); - - _cameraController = CameraController( - widget.camera, - ResolutionPreset.medium, - ); - - _initializeControllerFuture = _cameraController.initialize(); - } - - @override - void dispose() { - _cameraController.dispose(); - super.dispose(); - } - - Future _takePhoto(context) async { - try { - log.info('Taking photo for todo: ${widget.todoId}'); - await _initializeControllerFuture; - final XFile photo = await _cameraController.takePicture(); - - // Read the photo data as a stream - final photoFile = File(photo.path); - if (!await photoFile.exists()) { - log.warning('Photo file does not exist: ${photo.path}'); - return; - } - - final photoDataStream = photoFile.openRead().cast(); - - // Save the photo attachment directly with the data stream - final attachment = await savePhotoAttachment(photoDataStream, widget.todoId); - - log.info('Photo attachment saved with ID: ${attachment.id}'); - } catch (e) { - log.severe('Error taking photo: $e'); - } - Navigator.pop(context); - } - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FutureBuilder( - future: _initializeControllerFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - return CameraPreview(_cameraController); - } else { - return const CircularProgressIndicator(); - } - }, - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () => _takePhoto(context), - child: const Text('Take Photo'), - ), - ], - ); - } -} diff --git a/demos/supabase-todolist-new-attachment/lib/attachments/photo_widget.dart b/demos/supabase-todolist-new-attachment/lib/attachments/photo_widget.dart deleted file mode 100644 index 5d11276a..00000000 --- a/demos/supabase-todolist-new-attachment/lib/attachments/photo_widget.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:logging/logging.dart'; -import 'package:powersync_attachments_stream/powersync_attachments_stream.dart'; -import 'package:powersync_flutter_demo_new/attachments/camera_helpers.dart'; -import 'package:powersync_flutter_demo_new/attachments/photo_capture_widget.dart'; -import 'package:powersync_flutter_demo_new/attachments/queue.dart'; - -import '../models/todo_item.dart'; - -class PhotoWidget extends StatefulWidget { - final TodoItem todo; - - PhotoWidget({ - required this.todo, - }) : super(key: ObjectKey(todo.id)); - - @override - State createState() { - return _PhotoWidgetState(); - } -} - -class _ResolvedPhotoState { - String? photoPath; - bool fileExists; - Attachment? attachment; - - _ResolvedPhotoState( - {required this.photoPath, required this.fileExists, this.attachment}); -} - -class _PhotoWidgetState extends State { - late String photoPath; - final log = Logger('PhotoWidget'); - - Future<_ResolvedPhotoState> _getPhotoState(photoId) async { - if (photoId == null) { - return _ResolvedPhotoState(photoPath: null, fileExists: false); - } - - photoPath = attachmentQueue.getLocalUri('$photoId.jpg'); - - bool fileExists = await File(photoPath).exists(); - - final row = await attachmentQueue.db - .getOptional('SELECT * FROM attachments_queue WHERE id = ?', [photoId]); - - log.info('row: $row'); - - if (row != null) { - Attachment attachment = Attachment.fromRow(row); - - return _ResolvedPhotoState( - photoPath: photoPath, fileExists: fileExists, attachment: attachment); - } - - return _ResolvedPhotoState( - photoPath: photoPath, fileExists: fileExists, attachment: null); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: _getPhotoState(widget.todo.photoId), - builder: (BuildContext context, - AsyncSnapshot<_ResolvedPhotoState> snapshot) { - if (snapshot.data == null) { - return Container(); - } - final data = snapshot.data!; - Widget takePhotoButton = ElevatedButton( - onPressed: () async { - final camera = await setupCamera(); - if (!context.mounted) return; - - if (camera == null) { - const snackBar = SnackBar( - content: Text('No camera available'), - backgroundColor: - Colors.red, // Optional: to highlight it's an error - ); - - ScaffoldMessenger.of(context).showSnackBar(snackBar); - return; - } - - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - TakePhotoWidget(todoId: widget.todo.id, camera: camera), - ), - ); - }, - child: const Text('Take Photo'), - ); - - if (widget.todo.photoId == null) { - return takePhotoButton; - } - - String? filePath = data.photoPath; - bool fileIsDownloading = !data.fileExists; - bool fileArchived = - data.attachment?.state == AttachmentState.archived; - - if (fileArchived) { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text("Unavailable"), - const SizedBox(height: 8), - takePhotoButton - ], - ); - } - - if (fileIsDownloading) { - return const Text("Downloading..."); - } - - File imageFile = File(filePath!); - int lastModified = imageFile.existsSync() - ? imageFile.lastModifiedSync().millisecondsSinceEpoch - : 0; - Key key = ObjectKey('$filePath:$lastModified'); - - return Image.file( - key: key, - imageFile, - width: 50, - height: 50, - ); - }); - } -} diff --git a/demos/supabase-todolist-new-attachment/lib/attachments/queue.dart b/demos/supabase-todolist-new-attachment/lib/attachments/queue.dart deleted file mode 100644 index e4e7b170..00000000 --- a/demos/supabase-todolist-new-attachment/lib/attachments/queue.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:typed_data'; -import 'package:logging/logging.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:powersync/powersync.dart'; -import 'package:powersync_attachments_stream/powersync_attachments_stream.dart'; -import 'package:powersync_flutter_demo_new/attachments/remote_storage_adapter.dart'; - -late AttachmentQueue attachmentQueue; -final remoteStorage = SupabaseStorageAdapter(); -final log = Logger('AttachmentQueue'); - -Future onDownloadError(Attachment attachment, Object exception) async { - if (exception.toString().contains('Object not found')) { - return false; - } - return true; -} - -Future initializeAttachmentQueue(PowerSyncDatabase db) async { - // Use the app's document directory for local storage - final Directory appDocDir = await getApplicationDocumentsDirectory(); - final localStorage = IOLocalStorage(appDocDir); - - log.info('directory: ${appDocDir.path}'); - log.info('localStorage: $localStorage'); - - attachmentQueue = AttachmentQueue( - db: db, - remoteStorage: remoteStorage, - attachmentsDirectory: '${appDocDir.path}/attachments', - watchAttachments: () => db.watch(''' - SELECT photo_id as id FROM todos WHERE photo_id IS NOT NULL - ''').map((results) { - final items = results.map((row) => WatchedAttachmentItem(id: row['id'] as String, fileExtension: 'jpg')).toList(); - log.info('Watched attachment IDs: ${items.map((e) => e.id).toList()}'); - return items; - }), - localStorage: localStorage, - errorHandler: null, - ); -} - -Future savePhotoAttachment(Stream photoData, String todoId, - {String mediaType = 'image/jpeg'}) async { - log.info('Saving photo attachment for todo: $todoId'); - - // Save the file using the AttachmentQueue API - return await attachmentQueue.saveFile( - data: photoData, - mediaType: mediaType, - fileExtension: 'jpg', - metaData: 'Photo attachment for todo: $todoId', - updateHook: (context, attachment) async { - // Update the todo item to reference this attachment - await context.execute( - 'UPDATE todos SET photo_id = ? WHERE id = ?', - [attachment.id, todoId], - ); - }, - ); -} - -Future deletePhotoAttachment(String fileId) async { - log.info('deletePhotoAttachment: $fileId'); - return await attachmentQueue.deleteFile( - attachmentId: fileId, - updateHook: (context, attachment) async { - // Optionally update relationships in the same transaction - }, - ); -} diff --git a/demos/supabase-todolist-new-attachment/lib/attachments/remote_storage_adapter.dart b/demos/supabase-todolist-new-attachment/lib/attachments/remote_storage_adapter.dart deleted file mode 100644 index 2965b015..00000000 --- a/demos/supabase-todolist-new-attachment/lib/attachments/remote_storage_adapter.dart +++ /dev/null @@ -1,104 +0,0 @@ -import 'dart:io'; -import 'dart:typed_data'; -import 'package:powersync_attachments_stream/powersync_attachments_stream.dart'; -import 'package:powersync_flutter_demo_new/app_config.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; -import 'package:logging/logging.dart'; - -class SupabaseStorageAdapter implements RemoteStorage { - static final _log = Logger('SupabaseStorageAdapter'); - - @override - Future uploadFile( - Stream> fileData, Attachment attachment) async { - _checkSupabaseBucketIsConfigured(); - - // Check if attachment size is specified (required for buffer allocation) - final byteSize = attachment.size; - if (byteSize == null) { - throw Exception('Cannot upload a file with no byte size specified'); - } - - _log.info('uploadFile: ${attachment.filename} (size: $byteSize bytes)'); - - // Collect all stream data into a single Uint8List buffer (like Kotlin version) - final buffer = Uint8List(byteSize); - var position = 0; - - await for (final chunk in fileData) { - if (position + chunk.length > byteSize) { - throw Exception('File data exceeds specified size'); - } - buffer.setRange(position, position + chunk.length, chunk); - position += chunk.length; - } - - if (position != byteSize) { - throw Exception( - 'File data size ($position) does not match specified size ($byteSize)'); - } - - // Create a temporary file from the buffer for upload - final tempFile = - File('${Directory.systemTemp.path}/${attachment.filename}'); - try { - await tempFile.writeAsBytes(buffer); - - await Supabase.instance.client.storage - .from(AppConfig.supabaseStorageBucket) - .upload(attachment.filename, tempFile, - fileOptions: FileOptions( - contentType: - attachment.mediaType ?? 'application/octet-stream')); - - _log.info('Successfully uploaded ${attachment.filename}'); - } catch (error) { - _log.severe('Error uploading ${attachment.filename}', error); - throw Exception(error); - } finally { - if (await tempFile.exists()) { - await tempFile.delete(); - } - } - } - - @override - Future>> downloadFile(Attachment attachment) async { - _checkSupabaseBucketIsConfigured(); - try { - _log.info('downloadFile: ${attachment.filename}'); - - Uint8List fileBlob = await Supabase.instance.client.storage - .from(AppConfig.supabaseStorageBucket) - .download(attachment.filename); - - _log.info( - 'Successfully downloaded ${attachment.filename} (${fileBlob.length} bytes)'); - - // Return the raw file data as a stream - return Stream.value(fileBlob); - } catch (error) { - _log.severe('Error downloading ${attachment.filename}', error); - throw Exception(error); - } - } - - @override - Future deleteFile(Attachment attachment) async { - _checkSupabaseBucketIsConfigured(); - try { - await Supabase.instance.client.storage - .from(AppConfig.supabaseStorageBucket) - .remove([attachment.filename]); - } catch (error) { - throw Exception(error); - } - } - - void _checkSupabaseBucketIsConfigured() { - if (AppConfig.supabaseStorageBucket.isEmpty) { - throw Exception( - 'Supabase storage bucket is not configured in app_config.dart'); - } - } -} diff --git a/demos/supabase-todolist-new-attachment/lib/fts_helpers.dart b/demos/supabase-todolist-new-attachment/lib/fts_helpers.dart deleted file mode 100644 index cbad8480..00000000 --- a/demos/supabase-todolist-new-attachment/lib/fts_helpers.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:powersync_flutter_demo_new/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/supabase-todolist-new-attachment/lib/main.dart b/demos/supabase-todolist-new-attachment/lib/main.dart deleted file mode 100644 index 2e3eeea9..00000000 --- a/demos/supabase-todolist-new-attachment/lib/main.dart +++ /dev/null @@ -1,132 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:logging/logging.dart'; -import 'package:powersync_flutter_demo_new/app_config.dart'; -import 'package:powersync_flutter_demo_new/attachments/queue.dart'; -import 'package:powersync_flutter_demo_new/models/schema.dart'; - -import './powersync.dart'; -import './widgets/lists_page.dart'; -import './widgets/login_page.dart'; -import './widgets/query_widget.dart'; -import './widgets/signup_page.dart'; -import './widgets/status_app_bar.dart'; - -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(); - - if (AppConfig.supabaseStorageBucket.isNotEmpty) { - await initializeAttachmentQueue(db); - attachmentQueue.startSync(); - } - - final loggedIn = isLoggedIn(); - runApp(MyApp(loggedIn: loggedIn)); -} - -const defaultQuery = 'SELECT * from $todosTable'; - -const listsPage = ListsPage(); -const homePage = listsPage; - -const sqlConsolePage = Scaffold( - appBar: StatusAppBar(title: Text('SQL Console')), - body: QueryWidget(defaultQuery: defaultQuery)); - -const loginPage = LoginPage(); - -const signupPage = SignupPage(); - -class MyApp extends StatelessWidget { - final bool loggedIn; - - const MyApp({super.key, required this.loggedIn}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'PowerSync Flutter Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: loggedIn ? homePage : loginPage); - } -} - -class MyHomePage extends StatelessWidget { - const MyHomePage( - {super.key, - required this.title, - required this.content, - this.floatingActionButton}); - - final String title; - final Widget content; - final Widget? floatingActionButton; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: StatusAppBar(title: Text(title)), - body: Center(child: content), - floatingActionButton: floatingActionButton, - drawer: Drawer( - // Add a ListView to the drawer. This ensures the user can scroll - // through the options in the drawer if there isn't enough vertical - // space to fit everything. - child: ListView( - // Important: Remove any padding from the ListView. - padding: EdgeInsets.zero, - children: [ - const DrawerHeader( - decoration: BoxDecoration( - color: Colors.blue, - ), - child: Text(''), - ), - ListTile( - title: const Text('SQL Console'), - onTap: () { - var navigator = Navigator.of(context); - navigator.pop(); - - navigator.push(MaterialPageRoute( - builder: (context) => sqlConsolePage, - )); - }, - ), - ListTile( - title: const Text('Sign Out'), - onTap: () async { - var navigator = Navigator.of(context); - navigator.pop(); - await logout(); - - navigator.pushReplacement(MaterialPageRoute( - builder: (context) => loginPage, - )); - }, - ), - ], - ), - ), - ); - } -} diff --git a/demos/supabase-todolist-new-attachment/lib/migrations/fts_setup.dart b/demos/supabase-todolist-new-attachment/lib/migrations/fts_setup.dart deleted file mode 100644 index 9754e44e..00000000 --- a/demos/supabase-todolist-new-attachment/lib/migrations/fts_setup.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:powersync/powersync.dart'; -import 'package:powersync/sqlite_async.dart'; - -import 'helpers.dart'; -import '../models/schema.dart'; - -final migrations = SqliteMigrations(); - -/// Create a Full Text Search table for the given table and columns -/// with an option to use a different tokenizer otherwise it defaults -/// to unicode61. It also creates the triggers that keep the FTS table -/// and the PowerSync table in sync. -SqliteMigration createFtsMigration( - {required int migrationVersion, - required String tableName, - required List columns, - String tokenizationMethod = 'unicode61'}) { - String internalName = - schema.tables.firstWhere((table) => table.name == tableName).internalName; - String stringColumns = columns.join(', '); - - return SqliteMigration(migrationVersion, (tx) async { - // Add FTS table - await tx.execute(''' - CREATE VIRTUAL TABLE IF NOT EXISTS fts_$tableName - USING fts5(id UNINDEXED, $stringColumns, tokenize='$tokenizationMethod'); - '''); - // Copy over records already in table - await tx.execute(''' - INSERT INTO fts_$tableName(rowid, id, $stringColumns) - SELECT rowid, id, ${generateJsonExtracts(ExtractType.columnOnly, 'data', columns)} FROM $internalName; - '''); - // Add INSERT, UPDATE and DELETE and triggers to keep fts table in sync with table - await tx.execute(''' - CREATE TRIGGER IF NOT EXISTS fts_insert_trigger_$tableName AFTER INSERT ON $internalName - BEGIN - INSERT INTO fts_$tableName(rowid, id, $stringColumns) - VALUES ( - NEW.rowid, - NEW.id, - ${generateJsonExtracts(ExtractType.columnOnly, 'NEW.data', columns)} - ); - END; - '''); - await tx.execute(''' - CREATE TRIGGER IF NOT EXISTS fts_update_trigger_$tableName AFTER UPDATE ON $internalName BEGIN - UPDATE fts_$tableName - SET ${generateJsonExtracts(ExtractType.columnInOperation, 'NEW.data', columns)} - WHERE rowid = NEW.rowid; - END; - '''); - await tx.execute(''' - CREATE TRIGGER IF NOT EXISTS fts_delete_trigger_$tableName AFTER DELETE ON $internalName BEGIN - DELETE FROM fts_$tableName WHERE rowid = OLD.rowid; - END; - '''); - }); -} - -/// This is where you can add more migrations to generate FTS tables -/// that correspond to the tables in your schema and populate them -/// with the data you would like to search on -Future configureFts(PowerSyncDatabase db) async { - migrations - ..add(createFtsMigration( - migrationVersion: 1, - tableName: 'lists', - columns: ['name'], - tokenizationMethod: 'porter unicode61')) - ..add(createFtsMigration( - migrationVersion: 2, - tableName: 'todos', - columns: ['description', 'list_id'], - )); - await migrations.migrate(db); -} diff --git a/demos/supabase-todolist-new-attachment/lib/migrations/helpers.dart b/demos/supabase-todolist-new-attachment/lib/migrations/helpers.dart deleted file mode 100644 index c1a779e1..00000000 --- a/demos/supabase-todolist-new-attachment/lib/migrations/helpers.dart +++ /dev/null @@ -1,38 +0,0 @@ -typedef ExtractGenerator = String Function(String, String); - -enum ExtractType { - columnOnly, - columnInOperation, -} - -typedef ExtractGeneratorMap = Map; - -String _createExtract(String jsonColumnName, String columnName) => - 'json_extract($jsonColumnName, \'\$.$columnName\')'; - -ExtractGeneratorMap extractGeneratorsMap = { - ExtractType.columnOnly: ( - String jsonColumnName, - String columnName, - ) => - _createExtract(jsonColumnName, columnName), - ExtractType.columnInOperation: ( - String jsonColumnName, - String columnName, - ) => - '$columnName = ${_createExtract(jsonColumnName, columnName)}', -}; - -String generateJsonExtracts( - ExtractType type, String jsonColumnName, List columns) { - ExtractGenerator? generator = extractGeneratorsMap[type]; - if (generator == null) { - throw StateError('Unexpected null generator for key: $type'); - } - - if (columns.length == 1) { - return generator(jsonColumnName, columns.first); - } - - return columns.map((column) => generator(jsonColumnName, column)).join(', '); -} diff --git a/demos/supabase-todolist-new-attachment/lib/models/schema.dart b/demos/supabase-todolist-new-attachment/lib/models/schema.dart deleted file mode 100644 index 0ca9722d..00000000 --- a/demos/supabase-todolist-new-attachment/lib/models/schema.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:powersync/powersync.dart'; -import 'package:powersync_attachments_stream/powersync_attachments_stream.dart'; - -const todosTable = 'todos'; - -Schema schema = Schema(([ - const Table(todosTable, [ - Column.text('list_id'), - Column.text('photo_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')]) - ]), - const Table('lists', [ - Column.text('created_at'), - Column.text('name'), - Column.text('owner_id') - ]), - AttachmentsQueueTable( - attachmentsQueueTableName: defaultAttachmentsQueueTableName) -])); diff --git a/demos/supabase-todolist-new-attachment/lib/models/todo_item.dart b/demos/supabase-todolist-new-attachment/lib/models/todo_item.dart deleted file mode 100644 index 5eb1de99..00000000 --- a/demos/supabase-todolist-new-attachment/lib/models/todo_item.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:powersync_flutter_demo_new/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 = ?', - [getUserId(), id]); - } - } - - Future delete() async { - await db.execute('DELETE FROM $todosTable WHERE id = ?', [id]); - } - - static Future addPhoto(String photoId, String id) async { - print('addPhoto: $photoId, $id'); - await db.execute( - 'UPDATE $todosTable SET photo_id = ? WHERE id = ?', [photoId, id]); - } -} diff --git a/demos/supabase-todolist-new-attachment/lib/models/todo_list.dart b/demos/supabase-todolist-new-attachment/lib/models/todo_list.dart deleted file mode 100644 index 86170e02..00000000 --- a/demos/supabase-todolist-new-attachment/lib/models/todo_list.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'package:powersync/powersync.dart'; -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); - }); - } - - static Stream watchSyncStatus() { - return db.statusStream; - } - - /// 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, 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, getUserId()]); - return TodoItem.fromRow(results.first); - } -} diff --git a/demos/supabase-todolist-new-attachment/lib/powersync.dart b/demos/supabase-todolist-new-attachment/lib/powersync.dart deleted file mode 100644 index f44aa8f4..00000000 --- a/demos/supabase-todolist-new-attachment/lib/powersync.dart +++ /dev/null @@ -1,200 +0,0 @@ -// This file performs setup of the PowerSync database -import 'package:flutter/foundation.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:powersync/powersync.dart'; -import 'package:powersync_flutter_demo_new/migrations/fts_setup.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; - -import './app_config.dart'; -import './models/schema.dart'; -import './supabase.dart'; - -final log = Logger('powersync-supabase'); - -/// Postgres Response codes that we cannot recover from by retrying. -final List fatalResponseCodes = [ - // Class 22 — Data Exception - // Examples include data type mismatch. - RegExp(r'^22...$'), - // Class 23 — Integrity Constraint Violation. - // Examples include NOT NULL, FOREIGN KEY and UNIQUE violations. - RegExp(r'^23...$'), - // INSUFFICIENT PRIVILEGE - typically a row-level security violation - RegExp(r'^42501$'), -]; - -/// Use Supabase for authentication and data upload. -class SupabaseConnector extends PowerSyncBackendConnector { - Future? _refreshFuture; - - SupabaseConnector(); - - /// Get a Supabase token to authenticate against the PowerSync instance. - @override - Future fetchCredentials() async { - // Wait for pending session refresh if any - await _refreshFuture; - - // Use Supabase token for PowerSync - final session = Supabase.instance.client.auth.currentSession; - if (session == null) { - // Not logged in - return null; - } - - // Use the access token to authenticate against PowerSync - final token = session.accessToken; - - // userId and expiresAt are for debugging purposes only - final userId = session.user.id; - final expiresAt = session.expiresAt == null - ? null - : DateTime.fromMillisecondsSinceEpoch(session.expiresAt! * 1000); - return PowerSyncCredentials( - endpoint: AppConfig.powersyncUrl, - token: token, - userId: userId, - expiresAt: expiresAt); - } - - @override - void invalidateCredentials() { - // Trigger a session refresh if auth fails on PowerSync. - // Generally, sessions should be refreshed automatically by Supabase. - // However, in some cases it can be a while before the session refresh is - // retried. We attempt to trigger the refresh as soon as we get an auth - // failure on PowerSync. - // - // This could happen if the device was offline for a while and the session - // expired, and nothing else attempt to use the session it in the meantime. - // - // Timeout the refresh call to avoid waiting for long retries, - // and ignore any errors. Errors will surface as expired tokens. - _refreshFuture = Supabase.instance.client.auth - .refreshSession() - .timeout(const Duration(seconds: 5)) - .then((response) => null, onError: (error) => null); - } - - // Upload pending changes to Supabase. - @override - Future uploadData(PowerSyncDatabase database) async { - // This function is called whenever there is data to upload, whether the - // device is online or offline. - // If this call throws an error, it is retried periodically. - final transaction = await database.getNextCrudTransaction(); - if (transaction == null) { - return; - } - - final rest = Supabase.instance.client.rest; - CrudEntry? lastOp; - try { - // Note: If transactional consistency is important, use database functions - // or edge functions to process the entire transaction in a single call. - for (var op in transaction.crud) { - lastOp = op; - - final table = rest.from(op.table); - if (op.op == UpdateType.put) { - var data = Map.of(op.opData!); - data['id'] = op.id; - await table.upsert(data); - } else if (op.op == UpdateType.patch) { - await table.update(op.opData!).eq('id', op.id); - } else if (op.op == UpdateType.delete) { - await table.delete().eq('id', op.id); - } - } - - // All operations successful. - await transaction.complete(); - } on PostgrestException catch (e) { - if (e.code != null && - fatalResponseCodes.any((re) => re.hasMatch(e.code!))) { - /// Instead of blocking the queue with these errors, - /// discard the (rest of the) transaction. - /// - /// Note that these errors typically indicate a bug in the application. - /// If protecting against data loss is important, save the failing records - /// elsewhere instead of discarding, and/or notify the user. - log.severe('Data upload error - discarding $lastOp', e); - await transaction.complete(); - } else { - // Error may be retryable - e.g. network error or temporary server error. - // Throwing an error here causes this call to be retried after a delay. - rethrow; - } - } - } -} - -/// Global reference to the database -late final PowerSyncDatabase db; - -bool isLoggedIn() { - return Supabase.instance.client.auth.currentSession?.accessToken != null; -} - -/// id of the user currently logged in -String? getUserId() { - return Supabase.instance.client.auth.currentSession?.user.id; -} - -Future getDatabasePath() async { - const dbFilename = 'powersync-demo-new-attachment.db'; - // getApplicationSupportDirectory is not supported on Web - if (kIsWeb) { - return dbFilename; - } - final dir = await getApplicationSupportDirectory(); - return join(dir.path, dbFilename); -} - -const options = SyncOptions(syncImplementation: SyncClientImplementation.rust); - -Future openDatabase() async { - // Open the local database - db = PowerSyncDatabase( - schema: schema, path: await getDatabasePath(), logger: attachedLogger); - await db.initialize(); - - await loadSupabase(); - - SupabaseConnector? currentConnector; - - if (isLoggedIn()) { - // If the user is already logged in, connect immediately. - // Otherwise, connect once logged in. - currentConnector = SupabaseConnector(); - db.connect(connector: currentConnector, options: options); - } - - Supabase.instance.client.auth.onAuthStateChange.listen((data) async { - final AuthChangeEvent event = data.event; - if (event == AuthChangeEvent.signedIn) { - // Connect to PowerSync when the user is signed in - currentConnector = SupabaseConnector(); - db.connect(connector: currentConnector!, options: options); - } else if (event == AuthChangeEvent.signedOut) { - // Implicit sign out - disconnect, but don't delete data - currentConnector = null; - await db.disconnect(); - } else if (event == AuthChangeEvent.tokenRefreshed) { - // Supabase token refreshed - trigger token refresh for PowerSync. - currentConnector?.prefetchCredentials(); - } - }); - - // Demo using SQLite Full-Text Search with PowerSync. - // See https://docs.powersync.com/usage-examples/full-text-search for more details - await configureFts(db); -} - -/// Explicit sign out - clear database and log out. -Future logout() async { - await Supabase.instance.client.auth.signOut(); - await db.disconnectAndClear(); -} diff --git a/demos/supabase-todolist-new-attachment/lib/supabase.dart b/demos/supabase-todolist-new-attachment/lib/supabase.dart deleted file mode 100644 index a69586e2..00000000 --- a/demos/supabase-todolist-new-attachment/lib/supabase.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:supabase_flutter/supabase_flutter.dart'; - -import './app_config.dart'; - -loadSupabase() async { - await Supabase.initialize( - url: AppConfig.supabaseUrl, - anonKey: AppConfig.supabaseAnonKey, - ); -} diff --git a/demos/supabase-todolist-new-attachment/lib/widgets/fts_search_delegate.dart b/demos/supabase-todolist-new-attachment/lib/widgets/fts_search_delegate.dart deleted file mode 100644 index 0b8d6bf7..00000000 --- a/demos/supabase-todolist-new-attachment/lib/widgets/fts_search_delegate.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:logging/logging.dart'; -import 'package:powersync_flutter_demo_new/fts_helpers.dart' as fts_helpers; -import 'package:powersync_flutter_demo_new/models/todo_list.dart'; - -import './todo_list_page.dart'; - -final log = Logger('powersync-supabase'); - -class FtsSearchDelegate extends SearchDelegate { - @override - List? buildActions(BuildContext context) { - return [ - IconButton( - onPressed: () { - query = ''; - }, - icon: const Icon(Icons.clear), - ), - ]; - } - - @override - Widget? buildLeading(BuildContext context) { - return IconButton( - onPressed: () { - close(context, null); - }, - icon: const Icon(Icons.arrow_back), - ); - } - - @override - Widget buildResults(BuildContext context) { - return FutureBuilder( - future: _search(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - return ListView.builder( - itemBuilder: (context, index) { - return ListTile( - title: Text(snapshot.data?[index].name), - onTap: () { - close(context, null); - }, - ); - }, - itemCount: snapshot.data?.length, - ); - } else { - return const Center( - child: CircularProgressIndicator(), - ); - } - }, - ); - } - - @override - Widget buildSuggestions(BuildContext context) { - NavigatorState navigator = Navigator.of(context); - - return FutureBuilder( - future: _search(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - return ListView.builder( - itemBuilder: (context, index) { - return ListTile( - title: Text(snapshot.data?[index]['name'] ?? ''), - onTap: () async { - 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 { - if (query.isEmpty) return []; - List listsSearchResults = await fts_helpers.search(query, 'lists'); - List todoItemsSearchResults = await fts_helpers.search(query, 'todos'); - List formattedListResults = listsSearchResults - .map((result) => {"id": result['id'], "name": result['name']}) - .toList(); - List formattedTodoItemsResults = todoItemsSearchResults - .map((result) => { - // Use list_id so the navigation goes to the list page - "id": result['list_id'], - "name": result['description'], - }) - .toList(); - List formattedResults = [ - ...formattedListResults, - ...formattedTodoItemsResults - ]; - return formattedResults; - } -} diff --git a/demos/supabase-todolist-new-attachment/lib/widgets/guard_by_sync.dart b/demos/supabase-todolist-new-attachment/lib/widgets/guard_by_sync.dart deleted file mode 100644 index dbd233e0..00000000 --- a/demos/supabase-todolist-new-attachment/lib/widgets/guard_by_sync.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:powersync/powersync.dart' hide Column; -import 'package:powersync_flutter_demo_new/powersync.dart'; - -/// A widget that shows [child] after a complete sync on the database has -/// completed and a progress bar before that. -class GuardBySync extends StatelessWidget { - final Widget child; - - /// When set, wait only for a complete sync within the [BucketPriority] - /// instead of a full sync. - final BucketPriority? priority; - - const GuardBySync({ - super.key, - required this.child, - this.priority, - }); - - @override - Widget build(BuildContext context) { - return StreamBuilder( - stream: db.statusStream, - initialData: db.currentStatus, - builder: (context, snapshot) { - final status = snapshot.requireData; - final (didSync, progress) = switch (priority) { - null => (status.hasSynced ?? false, status.downloadProgress), - var priority? => ( - status.statusForPriority(priority).hasSynced ?? false, - status.downloadProgress?.untilPriority(priority) - ), - }; - - if (didSync) { - return child; - } else { - return Center( - child: Column( - children: [ - const Text('Busy with sync...'), - LinearProgressIndicator(value: progress?.downloadedFraction), - if (progress case final progress?) - Text( - '${progress.downloadedOperations} out of ${progress.totalOperations}') - ], - ), - ); - } - }, - ); - } -} diff --git a/demos/supabase-todolist-new-attachment/lib/widgets/list_item.dart b/demos/supabase-todolist-new-attachment/lib/widgets/list_item.dart deleted file mode 100644 index 981b382a..00000000 --- a/demos/supabase-todolist-new-attachment/lib/widgets/list_item.dart +++ /dev/null @@ -1,59 +0,0 @@ -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/supabase-todolist-new-attachment/lib/widgets/list_item_dialog.dart b/demos/supabase-todolist-new-attachment/lib/widgets/list_item_dialog.dart deleted file mode 100644 index 3fb8c133..00000000 --- a/demos/supabase-todolist-new-attachment/lib/widgets/list_item_dialog.dart +++ /dev/null @@ -1,60 +0,0 @@ -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/supabase-todolist-new-attachment/lib/widgets/lists_page.dart b/demos/supabase-todolist-new-attachment/lib/widgets/lists_page.dart deleted file mode 100644 index c41aabbe..00000000 --- a/demos/supabase-todolist-new-attachment/lib/widgets/lists_page.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:powersync/powersync.dart'; - -import './list_item.dart'; -import './list_item_dialog.dart'; -import '../main.dart'; -import '../models/todo_list.dart'; -import 'guard_by_sync.dart'; - -void _showAddDialog(BuildContext context) async { - return showDialog( - context: context, - barrierDismissible: false, // user must tap button! - builder: (BuildContext context) { - return const ListItemDialog(); - }, - ); -} - -class ListsPage extends StatelessWidget { - const ListsPage({super.key}); - - @override - Widget build(BuildContext context) { - const content = ListsWidget(); - - final button = FloatingActionButton( - onPressed: () { - _showAddDialog(context); - }, - tooltip: 'Create List', - child: const Icon(Icons.add), - ); - - final page = MyHomePage( - title: 'Todo Lists', - content: content, - floatingActionButton: button, - ); - return page; - } -} - -class ListsWidget extends StatelessWidget { - const ListsWidget({super.key}); - - @override - Widget build(BuildContext context) { - return GuardBySync( - priority: _listsPriority, - child: StreamBuilder( - stream: TodoList.watchListsWithStats(), - builder: (context, snapshot) { - if (snapshot.data case final todoLists?) { - return ListView( - padding: const EdgeInsets.symmetric(vertical: 8.0), - children: todoLists.map((list) { - return ListItemWidget(list: list); - }).toList(), - ); - } else { - return const CircularProgressIndicator(); - } - }, - ), - ); - } - - static final _listsPriority = BucketPriority(1); -} diff --git a/demos/supabase-todolist-new-attachment/lib/widgets/login_page.dart b/demos/supabase-todolist-new-attachment/lib/widgets/login_page.dart deleted file mode 100644 index f54f09da..00000000 --- a/demos/supabase-todolist-new-attachment/lib/widgets/login_page.dart +++ /dev/null @@ -1,126 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:supabase_flutter/supabase_flutter.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 { - setState(() { - _busy = true; - _error = null; - }); - try { - await Supabase.instance.client.auth.signInWithPassword( - email: _usernameController.text, password: _passwordController.text); - - if (context.mounted) { - Navigator.of(context).pushReplacement(MaterialPageRoute( - builder: (context) => listsPage, - )); - } - } on AuthException catch (e) { - setState(() { - _error = e.message; - }); - } catch (e) { - setState(() { - _error = e.toString(); - }); - } finally { - setState(() { - _busy = false; - }); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text("PowerSync Flutter 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('Supabase 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'), - ), - TextButton( - onPressed: _busy - ? null - : () { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => signupPage, - )); - }, - child: const Text('Sign Up'), - ), - ], - ), - ), - ), - ), - ), - )); - } -} diff --git a/demos/supabase-todolist-new-attachment/lib/widgets/query_widget.dart b/demos/supabase-todolist-new-attachment/lib/widgets/query_widget.dart deleted file mode 100644 index 426c7060..00000000 --- a/demos/supabase-todolist-new-attachment/lib/widgets/query_widget.dart +++ /dev/null @@ -1,99 +0,0 @@ -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/supabase-todolist-new-attachment/lib/widgets/resultset_table.dart b/demos/supabase-todolist-new-attachment/lib/widgets/resultset_table.dart deleted file mode 100644 index f348e4ff..00000000 --- a/demos/supabase-todolist-new-attachment/lib/widgets/resultset_table.dart +++ /dev/null @@ -1,39 +0,0 @@ -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/supabase-todolist-new-attachment/lib/widgets/signup_page.dart b/demos/supabase-todolist-new-attachment/lib/widgets/signup_page.dart deleted file mode 100644 index 2f9150b4..00000000 --- a/demos/supabase-todolist-new-attachment/lib/widgets/signup_page.dart +++ /dev/null @@ -1,120 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; - -import '../main.dart'; - -class SignupPage extends StatefulWidget { - const SignupPage({super.key}); - - @override - State createState() => _SignupPageState(); -} - -class _SignupPageState 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 _signup(BuildContext context) async { - setState(() { - _busy = true; - _error = null; - }); - try { - final response = await Supabase.instance.client.auth.signUp( - email: _usernameController.text, password: _passwordController.text); - - if (context.mounted) { - if (response.session != null) { - Navigator.of(context).pushReplacement(MaterialPageRoute( - builder: (context) => homePage, - )); - } else { - Navigator.of(context).pop(); - } - } - } on AuthException catch (e) { - setState(() { - _error = e.message; - }); - } catch (e) { - setState(() { - _error = e.toString(); - }); - } finally { - setState(() { - _busy = false; - }); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text("PowerSync Flutter 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('Sign Up'), - const SizedBox(height: 35), - TextFormField( - controller: _usernameController, - decoration: const InputDecoration(labelText: "Email"), - enabled: !_busy, - onFieldSubmitted: _busy - ? null - : (String value) { - _signup(context); - }, - ), - const SizedBox(height: 20), - TextFormField( - obscureText: true, - controller: _passwordController, - decoration: InputDecoration( - labelText: "Password", errorText: _error), - enabled: !_busy, - onFieldSubmitted: _busy - ? null - : (String value) { - _signup(context); - }, - ), - const SizedBox(height: 25), - TextButton( - onPressed: _busy - ? null - : () { - _signup(context); - }, - child: const Text('Sign Up'), - ), - ], - ), - ), - ), - ), - ), - )); - } -} diff --git a/demos/supabase-todolist-new-attachment/lib/widgets/status_app_bar.dart b/demos/supabase-todolist-new-attachment/lib/widgets/status_app_bar.dart deleted file mode 100644 index a520aa19..00000000 --- a/demos/supabase-todolist-new-attachment/lib/widgets/status_app_bar.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:powersync/powersync.dart'; -import 'package:powersync_flutter_demo_new/widgets/fts_search_delegate.dart'; -import '../powersync.dart'; - -class StatusAppBar extends StatelessWidget implements PreferredSizeWidget { - final Widget title; - - const StatusAppBar({super.key, required this.title}); - - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); - - @override - Widget build(BuildContext context) { - return StreamBuilder( - stream: db.statusStream, - initialData: db.currentStatus, - builder: (context, snapshot) { - final status = snapshot.data!; - final statusIcon = _getStatusIcon(status); - - return AppBar( - title: title, - actions: [ - 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/supabase-todolist-new-attachment/lib/widgets/todo_item_dialog.dart b/demos/supabase-todolist-new-attachment/lib/widgets/todo_item_dialog.dart deleted file mode 100644 index 641abd7f..00000000 --- a/demos/supabase-todolist-new-attachment/lib/widgets/todo_item_dialog.dart +++ /dev/null @@ -1,65 +0,0 @@ -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 initState() { - super.initState(); - } - - @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/supabase-todolist-new-attachment/lib/widgets/todo_item_widget.dart b/demos/supabase-todolist-new-attachment/lib/widgets/todo_item_widget.dart deleted file mode 100644 index 66962790..00000000 --- a/demos/supabase-todolist-new-attachment/lib/widgets/todo_item_widget.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:powersync_flutter_demo_new/app_config.dart'; -import 'package:powersync_flutter_demo_new/attachments/photo_widget.dart'; -import 'package:powersync_flutter_demo_new/attachments/queue.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 { - if (todo.photoId != null) { - print('deleteTodo: ${todo.photoId}'); - - await attachmentQueue.deleteFile( - attachmentId: todo.photoId!, - updateHook: (context, attachment) async { - // await context.execute("UPDATE todos SET photo_id = NULL WHERE id = ?", [todo.id]); - }, - ); - } - await todo.delete(); - } - - @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', - ), - AppConfig.supabaseStorageBucket.isEmpty - ? Container() - : PhotoWidget(todo: todo), - ], - )); - } -} diff --git a/demos/supabase-todolist-new-attachment/lib/widgets/todo_list_page.dart b/demos/supabase-todolist-new-attachment/lib/widgets/todo_list_page.dart deleted file mode 100644 index 7e28238e..00000000 --- a/demos/supabase-todolist-new-attachment/lib/widgets/todo_list_page.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../powersync.dart'; -import './status_app_bar.dart'; -import './todo_item_dialog.dart'; -import './todo_item_widget.dart'; -import '../models/todo_list.dart'; - -void _showAddDialog(BuildContext context, TodoList list) async { - return showDialog( - context: context, - barrierDismissible: false, // user must tap button! - builder: (BuildContext context) { - return TodoItemDialog(list: list); - }, - ); -} - -class TodoListPage extends StatelessWidget { - final TodoList list; - - const TodoListPage({super.key, required this.list}); - - @override - Widget build(BuildContext context) { - final button = FloatingActionButton( - onPressed: () { - _showAddDialog(context, list); - }, - tooltip: 'Add Item', - child: const Icon(Icons.add), - ); - - return Scaffold( - appBar: StatusAppBar(title: Text(list.name)), - floatingActionButton: button, - body: TodoListWidget(list: list)); - } -} - -class TodoListWidget extends StatelessWidget { - final TodoList list; - - const TodoListWidget({super.key, required this.list}); - - @override - Widget build(BuildContext context) { - return StreamBuilder( - stream: TodoList.watchSyncStatus().map((e) => e.hasSynced), - initialData: db.currentStatus.hasSynced, - builder: (context, snapshot) { - return StreamBuilder( - stream: list.watchItems(), - builder: (context, snapshot) { - final items = snapshot.data ?? const []; - - return ListView( - padding: const EdgeInsets.symmetric(vertical: 8.0), - children: items.map((todo) { - return TodoItemWidget(todo: todo); - }).toList(), - ); - }, - ); - }, - ); - } -} diff --git a/demos/supabase-todolist-new-attachment/linux/.gitignore b/demos/supabase-todolist-new-attachment/linux/.gitignore deleted file mode 100644 index d3896c98..00000000 --- a/demos/supabase-todolist-new-attachment/linux/.gitignore +++ /dev/null @@ -1 +0,0 @@ -flutter/ephemeral diff --git a/demos/supabase-todolist-new-attachment/linux/CMakeLists.txt b/demos/supabase-todolist-new-attachment/linux/CMakeLists.txt deleted file mode 100644 index df0a3887..00000000 --- a/demos/supabase-todolist-new-attachment/linux/CMakeLists.txt +++ /dev/null @@ -1,138 +0,0 @@ -# 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/supabase-todolist-new-attachment/linux/flutter/CMakeLists.txt b/demos/supabase-todolist-new-attachment/linux/flutter/CMakeLists.txt deleted file mode 100644 index d5bd0164..00000000 --- a/demos/supabase-todolist-new-attachment/linux/flutter/CMakeLists.txt +++ /dev/null @@ -1,88 +0,0 @@ -# This file controls Flutter-level build steps. It should not be edited. -cmake_minimum_required(VERSION 3.10) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. - -# Serves the same purpose as list(TRANSFORM ... PREPEND ...), -# which isn't available in 3.10. -function(list_prepend LIST_NAME PREFIX) - set(NEW_LIST "") - foreach(element ${${LIST_NAME}}) - list(APPEND NEW_LIST "${PREFIX}${element}") - endforeach(element) - set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) -endfunction() - -# === Flutter Library === -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) -pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) -pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) - -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "fl_basic_message_channel.h" - "fl_binary_codec.h" - "fl_binary_messenger.h" - "fl_dart_project.h" - "fl_engine.h" - "fl_json_message_codec.h" - "fl_json_method_codec.h" - "fl_message_codec.h" - "fl_method_call.h" - "fl_method_channel.h" - "fl_method_codec.h" - "fl_method_response.h" - "fl_plugin_registrar.h" - "fl_plugin_registry.h" - "fl_standard_message_codec.h" - "fl_standard_method_codec.h" - "fl_string_codec.h" - "fl_value.h" - "fl_view.h" - "flutter_linux.h" -) -list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") -target_link_libraries(flutter INTERFACE - PkgConfig::GTK - PkgConfig::GLIB - PkgConfig::GIO -) -add_dependencies(flutter flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CMAKE_CURRENT_BINARY_DIR}/_phony_ - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" - ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} -) diff --git a/demos/supabase-todolist-new-attachment/linux/flutter/generated_plugin_registrant.cc b/demos/supabase-todolist-new-attachment/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 1bef6a30..00000000 --- a/demos/supabase-todolist-new-attachment/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,27 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include -#include -#include -#include - -void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) gtk_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); - gtk_plugin_register_with_registrar(gtk_registrar); - g_autoptr(FlPluginRegistrar) powersync_flutter_libs_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "PowersyncFlutterLibsPlugin"); - powersync_flutter_libs_plugin_register_with_registrar(powersync_flutter_libs_registrar); - g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); - sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); - g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); - url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); -} diff --git a/demos/supabase-todolist-new-attachment/linux/flutter/generated_plugin_registrant.h b/demos/supabase-todolist-new-attachment/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47b..00000000 --- a/demos/supabase-todolist-new-attachment/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/demos/supabase-todolist-new-attachment/linux/flutter/generated_plugins.cmake b/demos/supabase-todolist-new-attachment/linux/flutter/generated_plugins.cmake deleted file mode 100644 index ed77a1a0..00000000 --- a/demos/supabase-todolist-new-attachment/linux/flutter/generated_plugins.cmake +++ /dev/null @@ -1,27 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST - gtk - powersync_flutter_libs - sqlite3_flutter_libs - url_launcher_linux -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/demos/supabase-todolist-new-attachment/linux/main.cc b/demos/supabase-todolist-new-attachment/linux/main.cc deleted file mode 100644 index e7c5c543..00000000 --- a/demos/supabase-todolist-new-attachment/linux/main.cc +++ /dev/null @@ -1,6 +0,0 @@ -#include "my_application.h" - -int main(int argc, char** argv) { - g_autoptr(MyApplication) app = my_application_new(); - return g_application_run(G_APPLICATION(app), argc, argv); -} diff --git a/demos/supabase-todolist-new-attachment/linux/my_application.cc b/demos/supabase-todolist-new-attachment/linux/my_application.cc deleted file mode 100644 index 7dcb7e37..00000000 --- a/demos/supabase-todolist-new-attachment/linux/my_application.cc +++ /dev/null @@ -1,104 +0,0 @@ -#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/supabase-todolist-new-attachment/linux/my_application.h b/demos/supabase-todolist-new-attachment/linux/my_application.h deleted file mode 100644 index 72271d5e..00000000 --- a/demos/supabase-todolist-new-attachment/linux/my_application.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef FLUTTER_MY_APPLICATION_H_ -#define FLUTTER_MY_APPLICATION_H_ - -#include - -G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, - GtkApplication) - -/** - * my_application_new: - * - * Creates a new Flutter-based application. - * - * Returns: a new #MyApplication. - */ -MyApplication* my_application_new(); - -#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/demos/supabase-todolist-new-attachment/linux/runner/CMakeLists.txt b/demos/supabase-todolist-new-attachment/linux/runner/CMakeLists.txt deleted file mode 100644 index e97dabc7..00000000 --- a/demos/supabase-todolist-new-attachment/linux/runner/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -cmake_minimum_required(VERSION 3.13) -project(runner LANGUAGES CXX) - -# Define the application target. To change its name, change BINARY_NAME in the -# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer -# work. -# -# Any new source files that you add to the application should be added here. -add_executable(${BINARY_NAME} - "main.cc" - "my_application.cc" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" -) - -# Apply the standard set of build settings. This can be removed for applications -# that need different build settings. -apply_standard_settings(${BINARY_NAME}) - -# Add preprocessor definitions for the application ID. -add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") - -# Add dependency libraries. Add any application-specific dependencies here. -target_link_libraries(${BINARY_NAME} PRIVATE flutter) -target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) - -target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/demos/supabase-todolist-new-attachment/linux/runner/main.cc b/demos/supabase-todolist-new-attachment/linux/runner/main.cc deleted file mode 100644 index e7c5c543..00000000 --- a/demos/supabase-todolist-new-attachment/linux/runner/main.cc +++ /dev/null @@ -1,6 +0,0 @@ -#include "my_application.h" - -int main(int argc, char** argv) { - g_autoptr(MyApplication) app = my_application_new(); - return g_application_run(G_APPLICATION(app), argc, argv); -} diff --git a/demos/supabase-todolist-new-attachment/linux/runner/my_application.cc b/demos/supabase-todolist-new-attachment/linux/runner/my_application.cc deleted file mode 100644 index dde6b0ac..00000000 --- a/demos/supabase-todolist-new-attachment/linux/runner/my_application.cc +++ /dev/null @@ -1,130 +0,0 @@ -#include "my_application.h" - -#include -#ifdef GDK_WINDOWING_X11 -#include -#endif - -#include "flutter/generated_plugin_registrant.h" - -struct _MyApplication { - GtkApplication parent_instance; - char** dart_entrypoint_arguments; -}; - -G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) - -// Implements GApplication::activate. -static void my_application_activate(GApplication* application) { - MyApplication* self = MY_APPLICATION(application); - GtkWindow* window = - GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); - - // Use a header bar when running in GNOME as this is the common style used - // by applications and is the setup most users will be using (e.g. Ubuntu - // desktop). - // If running on X and not using GNOME then just use a traditional title bar - // in case the window manager does more exotic layout, e.g. tiling. - // If running on Wayland assume the header bar will work (may need changing - // if future cases occur). - gboolean use_header_bar = TRUE; -#ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(window); - if (GDK_IS_X11_SCREEN(screen)) { - const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); - if (g_strcmp0(wm_name, "GNOME Shell") != 0) { - use_header_bar = FALSE; - } - } -#endif - if (use_header_bar) { - GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); - gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "powersync_flutter_demo"); - gtk_header_bar_set_show_close_button(header_bar, TRUE); - gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); - } else { - gtk_window_set_title(window, "powersync_flutter_demo"); - } - - gtk_window_set_default_size(window, 1280, 720); - gtk_widget_show(GTK_WIDGET(window)); - - g_autoptr(FlDartProject) project = fl_dart_project_new(); - fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); - - FlView* view = fl_view_new(project); - gtk_widget_show(GTK_WIDGET(view)); - gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); - - fl_register_plugins(FL_PLUGIN_REGISTRY(view)); - - gtk_widget_grab_focus(GTK_WIDGET(view)); -} - -// Implements GApplication::local_command_line. -static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { - MyApplication* self = MY_APPLICATION(application); - // Strip out the first argument as it is the binary name. - self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); - - g_autoptr(GError) error = nullptr; - if (!g_application_register(application, nullptr, &error)) { - g_warning("Failed to register: %s", error->message); - *exit_status = 1; - return TRUE; - } - - g_application_activate(application); - *exit_status = 0; - - return TRUE; -} - -// Implements GApplication::startup. -static void my_application_startup(GApplication* application) { - //MyApplication* self = MY_APPLICATION(object); - - // Perform any actions required at application startup. - - G_APPLICATION_CLASS(my_application_parent_class)->startup(application); -} - -// Implements GApplication::shutdown. -static void my_application_shutdown(GApplication* application) { - //MyApplication* self = MY_APPLICATION(object); - - // Perform any actions required at application shutdown. - - G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); -} - -// Implements GObject::dispose. -static void my_application_dispose(GObject* object) { - MyApplication* self = MY_APPLICATION(object); - g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); - G_OBJECT_CLASS(my_application_parent_class)->dispose(object); -} - -static void my_application_class_init(MyApplicationClass* klass) { - G_APPLICATION_CLASS(klass)->activate = my_application_activate; - G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; - G_APPLICATION_CLASS(klass)->startup = my_application_startup; - G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; - G_OBJECT_CLASS(klass)->dispose = my_application_dispose; -} - -static void my_application_init(MyApplication* self) {} - -MyApplication* my_application_new() { - // Set the program name to the application ID, which helps various systems - // like GTK and desktop environments map this running application to its - // corresponding .desktop file. This ensures better integration by allowing - // the application to be recognized beyond its binary name. - g_set_prgname(APPLICATION_ID); - - return MY_APPLICATION(g_object_new(my_application_get_type(), - "application-id", APPLICATION_ID, - "flags", G_APPLICATION_NON_UNIQUE, - nullptr)); -} diff --git a/demos/supabase-todolist-new-attachment/linux/runner/my_application.h b/demos/supabase-todolist-new-attachment/linux/runner/my_application.h deleted file mode 100644 index 72271d5e..00000000 --- a/demos/supabase-todolist-new-attachment/linux/runner/my_application.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef FLUTTER_MY_APPLICATION_H_ -#define FLUTTER_MY_APPLICATION_H_ - -#include - -G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, - GtkApplication) - -/** - * my_application_new: - * - * Creates a new Flutter-based application. - * - * Returns: a new #MyApplication. - */ -MyApplication* my_application_new(); - -#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/demos/supabase-todolist-new-attachment/macos/.gitignore b/demos/supabase-todolist-new-attachment/macos/.gitignore deleted file mode 100644 index 746adbb6..00000000 --- a/demos/supabase-todolist-new-attachment/macos/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Flutter-related -**/Flutter/ephemeral/ -**/Pods/ - -# Xcode-related -**/dgph -**/xcuserdata/ diff --git a/demos/supabase-todolist-new-attachment/macos/Flutter/Flutter-Debug.xcconfig b/demos/supabase-todolist-new-attachment/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index 4b81f9b2..00000000 --- a/demos/supabase-todolist-new-attachment/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/demos/supabase-todolist-new-attachment/macos/Flutter/Flutter-Release.xcconfig b/demos/supabase-todolist-new-attachment/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index 5caa9d15..00000000 --- a/demos/supabase-todolist-new-attachment/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/demos/supabase-todolist-new-attachment/macos/Flutter/GeneratedPluginRegistrant.swift b/demos/supabase-todolist-new-attachment/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index 0c6fd7ff..00000000 --- a/demos/supabase-todolist-new-attachment/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -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 - -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-new-attachment/macos/Podfile b/demos/supabase-todolist-new-attachment/macos/Podfile deleted file mode 100644 index c795730d..00000000 --- a/demos/supabase-todolist-new-attachment/macos/Podfile +++ /dev/null @@ -1,43 +0,0 @@ -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/supabase-todolist-new-attachment/macos/Podfile.lock b/demos/supabase-todolist-new-attachment/macos/Podfile.lock deleted file mode 100644 index f32a7411..00000000 --- a/demos/supabase-todolist-new-attachment/macos/Podfile.lock +++ /dev/null @@ -1,83 +0,0 @@ -PODS: - - app_links (1.0.0): - - FlutterMacOS - - FlutterMacOS (1.0.0) - - path_provider_foundation (0.0.1): - - Flutter - - FlutterMacOS - - powersync-sqlite-core (0.4.2) - - powersync_flutter_libs (0.0.1): - - FlutterMacOS - - powersync-sqlite-core (~> 0.4.2) - - shared_preferences_foundation (0.0.1): - - Flutter - - FlutterMacOS - - sqlite3 (3.49.2): - - sqlite3/common (= 3.49.2) - - sqlite3/common (3.49.2) - - sqlite3/dbstatvtab (3.49.2): - - sqlite3/common - - sqlite3/fts5 (3.49.2): - - sqlite3/common - - sqlite3/math (3.49.2): - - sqlite3/common - - sqlite3/perf-threadsafe (3.49.2): - - sqlite3/common - - sqlite3/rtree (3.49.2): - - sqlite3/common - - sqlite3_flutter_libs (0.0.1): - - Flutter - - FlutterMacOS - - sqlite3 (~> 3.49.1) - - sqlite3/dbstatvtab - - sqlite3/fts5 - - sqlite3/math - - sqlite3/perf-threadsafe - - sqlite3/rtree - - url_launcher_macos (0.0.1): - - FlutterMacOS - -DEPENDENCIES: - - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) - - FlutterMacOS (from `Flutter/ephemeral`) - - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - - powersync_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/powersync_flutter_libs/macos`) - - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`) - - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - -SPEC REPOS: - trunk: - - powersync-sqlite-core - - sqlite3 - -EXTERNAL SOURCES: - app_links: - :path: Flutter/ephemeral/.symlinks/plugins/app_links/macos - FlutterMacOS: - :path: Flutter/ephemeral - path_provider_foundation: - :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin - powersync_flutter_libs: - :path: Flutter/ephemeral/.symlinks/plugins/powersync_flutter_libs/macos - shared_preferences_foundation: - :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin - sqlite3_flutter_libs: - :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin - url_launcher_macos: - :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos - -SPEC CHECKSUMS: - app_links: afe860c55c7ef176cea7fb630a2b7d7736de591d - FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 - powersync-sqlite-core: a58efd88833861f0a8bb636c171bdf0ed55c9801 - powersync_flutter_libs: fa885a30ceb636655741eee2ff5282d0500fa96b - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 - sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 - url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 - -PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 - -COCOAPODS: 1.16.2 diff --git a/demos/supabase-todolist-new-attachment/macos/Runner.xcodeproj/project.pbxproj b/demos/supabase-todolist-new-attachment/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 5581abf6..00000000 --- a/demos/supabase-todolist-new-attachment/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,834 +0,0 @@ -// !$*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 */ - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 75E48BA0AEB945CF7281B8D7 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 610323F3498FEA93EE8D7ECD /* Pods_Runner.framework */; }; - 8B5261612A7C463D00E9899E /* powersync_flutter_demoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B5261602A7C463D00E9899E /* powersync_flutter_demoTests.swift */; }; - 9381A48772266D9C49309994 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 991B2149AEB0B3DAB09CB3BE /* Pods_RunnerTests.framework */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 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 */ - 10C22D07423926C20887DE48 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* powersync_flutter_demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = powersync_flutter_demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 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 = ""; }; - 4D425E3DE8C8153AB8C55A47 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 610323F3498FEA93EE8D7ECD /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 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 = ""; }; - 991B2149AEB0B3DAB09CB3BE /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - AE0A38BE3137E8F7E92FEF51 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - D7EC9DA661EA44265DC94A0B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - DD914308A8C7B352FD10170F /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - EE686A5A317D10AC330E1BF9 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 75E48BA0AEB945CF7281B8D7 /* Pods_Runner.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 8B52615B2A7C463D00E9899E /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9381A48772266D9C49309994 /* 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 */, - 85152821BF893F70AB178223 /* Pods */, - B7D43289954627B8B1B9B1F8 /* Frameworks */, - ); - 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 = ""; - }; - 85152821BF893F70AB178223 /* Pods */ = { - isa = PBXGroup; - children = ( - 4D425E3DE8C8153AB8C55A47 /* Pods-Runner.debug.xcconfig */, - D7EC9DA661EA44265DC94A0B /* Pods-Runner.release.xcconfig */, - EE686A5A317D10AC330E1BF9 /* Pods-Runner.profile.xcconfig */, - AE0A38BE3137E8F7E92FEF51 /* Pods-RunnerTests.debug.xcconfig */, - 10C22D07423926C20887DE48 /* Pods-RunnerTests.release.xcconfig */, - DD914308A8C7B352FD10170F /* Pods-RunnerTests.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; - 8B52615F2A7C463D00E9899E /* powersync_flutter_demoTests */ = { - isa = PBXGroup; - children = ( - 8B5261602A7C463D00E9899E /* powersync_flutter_demoTests.swift */, - ); - path = powersync_flutter_demoTests; - sourceTree = ""; - }; - B7D43289954627B8B1B9B1F8 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 610323F3498FEA93EE8D7ECD /* Pods_Runner.framework */, - 991B2149AEB0B3DAB09CB3BE /* 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 = ( - 179983970B20315AFC123D9D /* [CP] Check Pods Manifest.lock */, - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - F30CD4FEA1BDB49C8B84FB01 /* [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 = ( - 6C234BEFE02FE63D2DBBAA50 /* [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 */ - 179983970B20315AFC123D9D /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 3399D490228B24CF009A79C7 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - 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"; - }; - 6C234BEFE02FE63D2DBBAA50 /* [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; - }; - F30CD4FEA1BDB49C8B84FB01 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 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 = AE0A38BE3137E8F7E92FEF51 /* 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 = 10C22D07423926C20887DE48 /* 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 = DD914308A8C7B352FD10170F /* 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/supabase-todolist-new-attachment/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demos/supabase-todolist-new-attachment/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/demos/supabase-todolist-new-attachment/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/demos/supabase-todolist-new-attachment/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demos/supabase-todolist-new-attachment/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 943aed19..00000000 --- a/demos/supabase-todolist-new-attachment/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/demos/supabase-todolist-new-attachment/macos/Runner.xcworkspace/contents.xcworkspacedata b/demos/supabase-todolist-new-attachment/macos/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14..00000000 --- a/demos/supabase-todolist-new-attachment/macos/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/demos/supabase-todolist-new-attachment/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demos/supabase-todolist-new-attachment/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/demos/supabase-todolist-new-attachment/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/AppDelegate.swift b/demos/supabase-todolist-new-attachment/macos/Runner/AppDelegate.swift deleted file mode 100644 index b3c17614..00000000 --- a/demos/supabase-todolist-new-attachment/macos/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Cocoa -import FlutterMacOS - -@main -class AppDelegate: FlutterAppDelegate { - override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } - - override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { - return true - } -} diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index a2ec33f1..00000000 --- a/demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 82b6f9d9a33e198f5747104729e1fcef999772a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 102994 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? 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 z 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 Pe7HEdAEg|=c=LY&YVNkY diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index 13b35eba55c6dabc3aac36f33d859266c18fa0d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5680 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 diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index 0a3f5fa40fb3d1e0710331a48de5d256da3f275d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 520 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 diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/demos/supabase-todolist-new-attachment/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 2f1632cfddf3d9dade342351e627a0a75609fb46..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2218 zcmV;b2vzrqP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuE6iGxuRCodHTWf3-RTMruyW6Fu zQYeUM04eX6D5c0FCjKKPrco1(K`<0SL=crI{PC3-^hZU0kQie$gh-5!7z6SH6Q0J% zqot*`H1q{R5fHFYS}dje@;kG=v$L0(yY0?wY2%*c?A&{2?!D*x?m71{of2gv!$5|C z3>qG_BW}7K_yUcT3A5C6QD<+{aq?x;MAUyAiJn#Jv8_zZtQ{P zTRzbL3U9!qVuZzS$xKU10KiW~Bgdcv1-!uAhQxf3a7q+dU6lj?yoO4Lq4TUN4}h{N z*fIM=SS8|C2$(T>w$`t@3Tka!(r!7W`x z-isCVgQD^mG-MJ;XtJuK3V{Vy72GQ83KRWsHU?e*wrhKk=ApIYeDqLi;JI1e zuvv}5^Dc=k7F7?nm3nIw$NVmU-+R>> zyqOR$-2SDpJ}Pt;^RkJytDVXNTsu|mI1`~G7yw`EJR?VkGfNdqK9^^8P`JdtTV&tX4CNcV4 z&N06nZa??Fw1AgQOUSE2AmPE@WO(Fvo`%m`cDgiv(fAeRA%3AGXUbsGw{7Q`cY;1BI#ac3iN$$Hw z0LT0;xc%=q)me?Y*$xI@GRAw?+}>=9D+KTk??-HJ4=A>`V&vKFS75@MKdSF1JTq{S zc1!^8?YA|t+uKigaq!sT;Z!&0F2=k7F0PIU;F$leJLaw2UI6FL^w}OG&!;+b%ya1c z1n+6-inU<0VM-Y_s5iTElq)ThyF?StVcebpGI znw#+zLx2@ah{$_2jn+@}(zJZ{+}_N9BM;z)0yr|gF-4=Iyu@hI*Lk=-A8f#bAzc9f z`Kd6K--x@t04swJVC3JK1cHY-Hq+=|PN-VO;?^_C#;coU6TDP7Bt`;{JTG;!+jj(` zw5cLQ-(Cz-Tlb`A^w7|R56Ce;Wmr0)$KWOUZ6ai0PhzPeHwdl0H(etP zUV`va_i0s-4#DkNM8lUlqI7>YQLf)(lz9Q3Uw`)nc(z3{m5ZE77Ul$V%m)E}3&8L0 z-XaU|eB~Is08eORPk;=<>!1w)Kf}FOVS2l&9~A+@R#koFJ$Czd%Y(ENTV&A~U(IPI z;UY+gf+&6ioZ=roly<0Yst8ck>(M=S?B-ys3mLdM&)ex!hbt+ol|T6CTS+Sc0jv(& z7ijdvFwBq;0a{%3GGwkDKTeG`b+lyj0jjS1OMkYnepCdoosNY`*zmBIo*981BU%%U z@~$z0V`OVtIbEx5pa|Tct|Lg#ZQf5OYMUMRD>Wdxm5SAqV2}3!ceE-M2 z@O~lQ0OiKQp}o9I;?uxCgYVV?FH|?Riri*U$Zi_`V2eiA>l zdSm6;SEm6#T+SpcE8Ro_f2AwxzI z44hfe^WE3!h@W3RDyA_H440cpmYkv*)6m1XazTqw%=E5Xv7^@^^T7Q2wxr+Z2kVYr - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/Configs/AppInfo.xcconfig b/demos/supabase-todolist-new-attachment/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100644 index 370c9893..00000000 --- a/demos/supabase-todolist-new-attachment/macos/Runner/Configs/AppInfo.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// Application-level settings for the Runner target. -// -// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the -// future. If not, the values below would default to using the project name when this becomes a -// 'flutter create' template. - -// The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = PowerSync Supabase Demo - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = co.powersync.demotodolist - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2023 Journey Mobile, Inc. All rights reserved. diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/Configs/Debug.xcconfig b/demos/supabase-todolist-new-attachment/macos/Runner/Configs/Debug.xcconfig deleted file mode 100644 index 36b0fd94..00000000 --- a/demos/supabase-todolist-new-attachment/macos/Runner/Configs/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/Configs/Release.xcconfig b/demos/supabase-todolist-new-attachment/macos/Runner/Configs/Release.xcconfig deleted file mode 100644 index dff4f495..00000000 --- a/demos/supabase-todolist-new-attachment/macos/Runner/Configs/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/Configs/Warnings.xcconfig b/demos/supabase-todolist-new-attachment/macos/Runner/Configs/Warnings.xcconfig deleted file mode 100644 index 42bcbf47..00000000 --- a/demos/supabase-todolist-new-attachment/macos/Runner/Configs/Warnings.xcconfig +++ /dev/null @@ -1,13 +0,0 @@ -WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings -GCC_WARN_UNDECLARED_SELECTOR = YES -CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES -CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE -CLANG_WARN__DUPLICATE_METHOD_MATCH = YES -CLANG_WARN_PRAGMA_PACK = YES -CLANG_WARN_STRICT_PROTOTYPES = YES -CLANG_WARN_COMMA = YES -GCC_WARN_STRICT_SELECTOR_MATCH = YES -CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES -CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES -GCC_WARN_SHADOW = YES -CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/DebugProfile.entitlements b/demos/supabase-todolist-new-attachment/macos/Runner/DebugProfile.entitlements deleted file mode 100644 index 08c3ab17..00000000 --- a/demos/supabase-todolist-new-attachment/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,14 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.server - - com.apple.security.network.client - - - diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/Info.plist b/demos/supabase-todolist-new-attachment/macos/Runner/Info.plist deleted file mode 100644 index 4789daa6..00000000 --- a/demos/supabase-todolist-new-attachment/macos/Runner/Info.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - - diff --git a/demos/supabase-todolist-new-attachment/macos/Runner/MainFlutterWindow.swift b/demos/supabase-todolist-new-attachment/macos/Runner/MainFlutterWindow.swift deleted file mode 100644 index 2722837e..00000000 --- a/demos/supabase-todolist-new-attachment/macos/Runner/MainFlutterWindow.swift +++ /dev/null @@ -1,15 +0,0 @@ -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/supabase-todolist-new-attachment/macos/Runner/Release.entitlements b/demos/supabase-todolist-new-attachment/macos/Runner/Release.entitlements deleted file mode 100644 index ee95ab7e..00000000 --- a/demos/supabase-todolist-new-attachment/macos/Runner/Release.entitlements +++ /dev/null @@ -1,10 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.network.client - - - diff --git a/demos/supabase-todolist-new-attachment/macos/RunnerTests/RunnerTests.swift b/demos/supabase-todolist-new-attachment/macos/RunnerTests/RunnerTests.swift deleted file mode 100644 index 61f3bd1f..00000000 --- a/demos/supabase-todolist-new-attachment/macos/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Cocoa -import FlutterMacOS -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/demos/supabase-todolist-new-attachment/pubspec.lock b/demos/supabase-todolist-new-attachment/pubspec.lock deleted file mode 100644 index 8114b0f3..00000000 --- a/demos/supabase-todolist-new-attachment/pubspec.lock +++ /dev/null @@ -1,1061 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f - url: "https://pub.dev" - source: hosted - version: "85.0.0" - analyzer: - dependency: transitive - description: - name: analyzer - sha256: b1ade5707ab7a90dfd519eaac78a7184341d19adb6096c68d499b59c7c6cf880 - url: "https://pub.dev" - source: hosted - version: "7.7.0" - app_links: - dependency: transitive - description: - name: app_links - sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba" - url: "https://pub.dev" - source: hosted - version: "6.4.0" - app_links_linux: - dependency: transitive - description: - name: app_links_linux - sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 - url: "https://pub.dev" - source: hosted - version: "1.0.3" - app_links_platform_interface: - dependency: transitive - description: - name: app_links_platform_interface - sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" - url: "https://pub.dev" - source: hosted - version: "2.0.2" - app_links_web: - dependency: transitive - description: - name: app_links_web - sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 - url: "https://pub.dev" - source: hosted - version: "1.0.4" - archive: - dependency: transitive - description: - name: archive - sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" - url: "https://pub.dev" - source: hosted - version: "4.0.7" - args: - dependency: transitive - description: - name: args - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" - source: hosted - version: "2.7.0" - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - camera: - dependency: "direct main" - description: - name: camera - sha256: dfa8fc5a1adaeb95e7a54d86a5bd56f4bb0e035515354c8ac6d262e35cec2ec8 - url: "https://pub.dev" - source: hosted - version: "0.10.6" - camera_android: - dependency: transitive - description: - name: camera_android - sha256: "08808be7e26fc3c7426c81b3fa387564b8e9c22e6fe9cb5675ce3ab7017d8203" - url: "https://pub.dev" - source: hosted - version: "0.10.10+3" - camera_avfoundation: - dependency: transitive - description: - name: camera_avfoundation - sha256: ca36181194f429eef3b09de3c96280f2400693f9735025f90d1f4a27465fdd72 - url: "https://pub.dev" - source: hosted - version: "0.9.19" - camera_platform_interface: - dependency: transitive - description: - name: camera_platform_interface - sha256: "2f757024a48696ff4814a789b0bd90f5660c0fb25f393ab4564fb483327930e2" - url: "https://pub.dev" - source: hosted - version: "2.10.0" - camera_web: - dependency: transitive - description: - name: camera_web - sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f" - url: "https://pub.dev" - source: hosted - version: "0.3.5" - characters: - dependency: transitive - description: - name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff - url: "https://pub.dev" - source: hosted - version: "2.0.3" - cli_config: - dependency: transitive - description: - name: cli_config - sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec - url: "https://pub.dev" - source: hosted - version: "0.2.0" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - convert: - dependency: transitive - description: - name: convert - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.dev" - source: hosted - version: "3.1.2" - coverage: - dependency: transitive - description: - name: coverage - sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" - url: "https://pub.dev" - source: hosted - version: "1.15.0" - cross_file: - dependency: transitive - description: - name: cross_file - sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" - url: "https://pub.dev" - source: hosted - version: "0.3.4+2" - crypto: - dependency: transitive - description: - name: crypto - sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" - url: "https://pub.dev" - source: hosted - version: "3.0.6" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - flutter_plugin_android_lifecycle: - dependency: transitive - description: - name: flutter_plugin_android_lifecycle - sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e - url: "https://pub.dev" - source: hosted - version: "2.0.28" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - frontend_server_client: - dependency: transitive - description: - name: frontend_server_client - sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 - url: "https://pub.dev" - source: hosted - version: "4.0.0" - functions_client: - dependency: transitive - description: - name: functions_client - sha256: b410e4d609522357396cd84bb9a8f6e3a4561b5f7d3ce82267f6f1c2af42f16b - url: "https://pub.dev" - source: hosted - version: "2.4.2" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" - gotrue: - dependency: transitive - description: - name: gotrue - sha256: "04a6efacffd42773ed96dc752f19bb20a1fbc383e81ba82659072b775cf62912" - url: "https://pub.dev" - source: hosted - version: "2.12.0" - gtk: - dependency: transitive - description: - name: gtk - sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c - url: "https://pub.dev" - source: hosted - version: "2.1.0" - http: - dependency: transitive - description: - name: http - sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" - url: "https://pub.dev" - source: hosted - version: "1.4.0" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 - url: "https://pub.dev" - source: hosted - version: "3.2.2" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - image: - dependency: "direct main" - description: - name: image - sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" - url: "https://pub.dev" - source: hosted - version: "4.5.4" - io: - dependency: transitive - description: - name: io - sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b - url: "https://pub.dev" - source: hosted - version: "1.0.5" - js: - dependency: transitive - description: - name: js - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" - url: "https://pub.dev" - source: hosted - version: "0.7.2" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" - url: "https://pub.dev" - source: hosted - version: "4.9.0" - jwt_decode: - dependency: transitive - description: - name: jwt_decode - sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb - url: "https://pub.dev" - source: hosted - version: "0.3.1" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" - url: "https://pub.dev" - source: hosted - version: "10.0.9" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 - url: "https://pub.dev" - source: hosted - version: "3.0.9" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" - url: "https://pub.dev" - source: hosted - version: "3.0.1" - lints: - dependency: transitive - description: - name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 - url: "https://pub.dev" - source: hosted - version: "3.0.0" - logging: - dependency: "direct main" - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" - source: hosted - version: "0.12.17" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" - source: hosted - version: "0.11.1" - meta: - dependency: transitive - description: - name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c - url: "https://pub.dev" - source: hosted - version: "1.16.0" - mime: - dependency: transitive - description: - name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - mutex: - dependency: transitive - description: - name: mutex - sha256: "8827da25de792088eb33e572115a5eb0d61d61a3c01acbc8bcbe76ed78f1a1f2" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - node_preamble: - dependency: transitive - description: - name: node_preamble - sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" - url: "https://pub.dev" - source: hosted - version: "2.0.2" - package_config: - dependency: transitive - description: - name: package_config - sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.dev" - source: hosted - version: "2.2.0" - path: - dependency: "direct main" - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - path_provider: - dependency: "direct main" - description: - name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" - source: hosted - version: "2.1.5" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 - url: "https://pub.dev" - source: hosted - version: "2.2.17" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" - url: "https://pub.dev" - source: hosted - version: "6.1.0" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - pool: - dependency: transitive - description: - name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" - url: "https://pub.dev" - source: hosted - version: "1.5.1" - posix: - dependency: transitive - description: - name: posix - sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62 - url: "https://pub.dev" - source: hosted - version: "6.0.2" - postgrest: - dependency: transitive - description: - name: postgrest - sha256: "10b81a23b1c829ccadf68c626b4d66666453a1474d24c563f313f5ca7851d575" - url: "https://pub.dev" - source: hosted - version: "2.4.2" - powersync: - dependency: "direct main" - description: - path: "../../packages/powersync" - relative: true - source: path - version: "1.15.0" - powersync_attachments_helper: - dependency: "direct overridden" - description: - path: "../../packages/powersync_attachments_helper" - relative: true - source: path - version: "0.6.18+11" - powersync_attachments_stream: - dependency: "direct main" - description: - path: "../../packages/powersync_attachments_stream" - relative: true - source: path - version: "0.0.1" - powersync_core: - dependency: "direct overridden" - description: - path: "../../packages/powersync_core" - relative: true - source: path - version: "1.5.0" - powersync_flutter_libs: - dependency: "direct overridden" - description: - path: "../../packages/powersync_flutter_libs" - relative: true - source: path - version: "0.4.10" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - pubspec_parse: - dependency: transitive - description: - name: pubspec_parse - sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" - url: "https://pub.dev" - source: hosted - version: "1.5.0" - realtime_client: - dependency: transitive - description: - name: realtime_client - sha256: "3a0a99b5bd0fc3b35e8ee846d9a22fa2c2117f7ef1cb73d1e5f08f6c3d09c4e9" - url: "https://pub.dev" - source: hosted - version: "2.5.0" - retry: - dependency: transitive - description: - name: retry - sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" - url: "https://pub.dev" - source: hosted - version: "3.1.2" - rxdart: - dependency: transitive - description: - name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.dev" - source: hosted - version: "0.28.0" - shared_preferences: - dependency: transitive - description: - name: shared_preferences - sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" - url: "https://pub.dev" - source: hosted - version: "2.5.3" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" - url: "https://pub.dev" - source: hosted - version: "2.4.10" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" - url: "https://pub.dev" - source: hosted - version: "2.5.4" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.dev" - source: hosted - version: "2.4.3" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shelf: - dependency: transitive - description: - name: shelf - sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 - url: "https://pub.dev" - source: hosted - version: "1.4.2" - shelf_packages_handler: - dependency: transitive - description: - name: shelf_packages_handler - sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - shelf_static: - dependency: transitive - description: - name: shelf_static - sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 - url: "https://pub.dev" - source: hosted - version: "1.1.3" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - source_map_stack_trace: - dependency: transitive - description: - name: source_map_stack_trace - sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b - url: "https://pub.dev" - source: hosted - version: "2.1.2" - source_maps: - dependency: transitive - description: - name: source_maps - sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" - url: "https://pub.dev" - source: hosted - version: "0.10.13" - source_span: - dependency: transitive - description: - name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.dev" - source: hosted - version: "1.10.1" - sprintf: - dependency: transitive - description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.dev" - source: hosted - version: "7.0.0" - sqlite3: - dependency: transitive - description: - name: sqlite3 - sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e" - url: "https://pub.dev" - source: hosted - version: "2.7.5" - sqlite3_flutter_libs: - dependency: transitive - description: - name: sqlite3_flutter_libs - sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14" - url: "https://pub.dev" - source: hosted - version: "0.5.32" - sqlite3_web: - dependency: transitive - description: - name: sqlite3_web - sha256: "967e076442f7e1233bd7241ca61f3efe4c7fc168dac0f38411bdb3bdf471eb3c" - url: "https://pub.dev" - source: hosted - version: "0.3.1" - sqlite_async: - dependency: "direct main" - description: - name: sqlite_async - sha256: "9332aedd311a19dd215dcb55729bc68dc587dc7655b569ab8819b68ee0be0082" - url: "https://pub.dev" - source: hosted - version: "0.11.7" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - storage_client: - dependency: transitive - description: - name: storage_client - sha256: "09bac4d75eea58e8113ca928e6655a09cc8059e6d1b472ee801f01fde815bcfc" - url: "https://pub.dev" - source: hosted - version: "2.4.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - stream_transform: - dependency: transitive - description: - name: stream_transform - sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 - url: "https://pub.dev" - source: hosted - version: "2.1.1" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - supabase: - dependency: transitive - description: - name: supabase - sha256: f00172f5f0b2148ea1c573f52862d50cacb6f353f579f741fa35e51704845958 - url: "https://pub.dev" - source: hosted - version: "2.7.0" - supabase_flutter: - dependency: "direct main" - description: - name: supabase_flutter - sha256: d88eccf9e46e57129725a08e72a3109b6f780921fdc27fe3d7669a11ae80906b - url: "https://pub.dev" - source: hosted - version: "2.9.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test: - dependency: "direct dev" - description: - name: test - sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e" - url: "https://pub.dev" - source: hosted - version: "1.25.15" - test_api: - dependency: transitive - description: - name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd - url: "https://pub.dev" - source: hosted - version: "0.7.4" - test_core: - dependency: transitive - description: - name: test_core - sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" - url: "https://pub.dev" - source: hosted - version: "0.6.8" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - universal_io: - dependency: "direct main" - description: - name: universal_io - sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" - url: "https://pub.dev" - source: hosted - version: "2.2.2" - url_launcher: - dependency: transitive - description: - name: url_launcher - sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" - url: "https://pub.dev" - source: hosted - version: "6.3.1" - url_launcher_android: - dependency: transitive - description: - name: url_launcher_android - sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" - url: "https://pub.dev" - source: hosted - version: "6.3.16" - url_launcher_ios: - dependency: transitive - description: - name: url_launcher_ios - sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" - url: "https://pub.dev" - source: hosted - version: "6.3.3" - url_launcher_linux: - dependency: transitive - description: - name: url_launcher_linux - sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" - url: "https://pub.dev" - source: hosted - version: "3.2.1" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" - url: "https://pub.dev" - source: hosted - version: "3.2.2" - url_launcher_platform_interface: - dependency: transitive - description: - name: url_launcher_platform_interface - sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - url_launcher_web: - dependency: transitive - description: - name: url_launcher_web - sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - url_launcher_windows: - dependency: transitive - description: - name: url_launcher_windows - sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" - url: "https://pub.dev" - source: hosted - version: "3.1.4" - uuid: - dependency: transitive - description: - name: uuid - sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff - url: "https://pub.dev" - source: hosted - version: "4.5.1" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 - url: "https://pub.dev" - source: hosted - version: "15.0.0" - watcher: - dependency: transitive - description: - name: watcher - sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" - url: "https://pub.dev" - source: hosted - version: "1.1.2" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - web_socket: - dependency: transitive - description: - name: web_socket - sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 - url: "https://pub.dev" - source: hosted - version: "3.0.3" - webkit_inspection_protocol: - dependency: transitive - description: - name: webkit_inspection_protocol - sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" - url: "https://pub.dev" - source: hosted - version: "1.2.1" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - xml: - dependency: transitive - description: - name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 - url: "https://pub.dev" - source: hosted - version: "6.5.0" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" - yet_another_json_isolate: - dependency: transitive - description: - name: yet_another_json_isolate - sha256: fe45897501fa156ccefbfb9359c9462ce5dec092f05e8a56109db30be864f01e - url: "https://pub.dev" - source: hosted - version: "2.1.0" -sdks: - dart: ">=3.7.0 <4.0.0" - flutter: ">=3.27.0" diff --git a/demos/supabase-todolist-new-attachment/pubspec.yaml b/demos/supabase-todolist-new-attachment/pubspec.yaml deleted file mode 100644 index 1a46bd01..00000000 --- a/demos/supabase-todolist-new-attachment/pubspec.yaml +++ /dev/null @@ -1,34 +0,0 @@ -name: powersync_flutter_demo_new -description: PowerSync Flutter Demo -publish_to: "none" - -version: 1.0.1 - -environment: - sdk: ^3.4.0 - -dependencies: - flutter: - sdk: flutter - # powersync_attachments_helper: ^0.6.18+10 - powersync_attachments_stream: - path: ../../packages/powersync_attachments_stream - powersync: ^1.14.1 - path_provider: ^2.1.1 - supabase_flutter: ^2.0.1 - path: ^1.8.3 - logging: ^1.2.0 - camera: ^0.10.5+7 - image: ^4.1.3 - universal_io: ^2.2.2 - sqlite_async: ^0.11.0 - -dev_dependencies: - flutter_test: - sdk: flutter - - flutter_lints: ^3.0.1 - test: ^1.25.15 - -flutter: - uses-material-design: true diff --git a/demos/supabase-todolist-new-attachment/web/favicon.png b/demos/supabase-todolist-new-attachment/web/favicon.png deleted file mode 100644 index 8aaa46ac1ae21512746f852a42ba87e4165dfdd1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 917 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0X7 zltGxWVyS%@P(fs7NJL45ua8x7ey(0(N`6wRUPW#JP&EUCO@$SZnVVXYs8ErclUHn2 zVXFjIVFhG^g!Ppaz)DK8ZIvQ?0~DO|i&7O#^-S~(l1AfjnEK zjFOT9D}DX)@^Za$W4-*MbbUihOG|wNBYh(yU7!lx;>x^|#0uTKVr7USFmqf|i<65o z3raHc^AtelCMM;Vme?vOfh>Xph&xL%(-1c06+^uR^q@XSM&D4+Kp$>4P^%3{)XKjo zGZknv$b36P8?Z_gF{nK@`XI}Z90TzwSQO}0J1!f2c(B=V`5aP@1P1a|PZ!4!3&Gl8 zTYqUsf!gYFyJnXpu0!n&N*SYAX-%d(5gVjrHJWqXQshj@!Zm{!01WsQrH~9=kTxW#6SvuapgMqt>$=j#%eyGrQzr zP{L-3gsMA^$I1&gsBAEL+vxi1*Igl=8#8`5?A-T5=z-sk46WA1IUT)AIZHx1rdUrf zVJrJn<74DDw`j)Ki#gt}mIT-Q`XRa2-jQXQoI%w`nb|XblvzK${ZzlV)m-XcwC(od z71_OEC5Bt9GEXosOXaPTYOia#R4ID2TiU~`zVMl08TV_C%DnU4^+HE>9(CE4D6?Fz oujB08i7adh9xk7*FX66dWH6F5TM;?E2b5PlUHx3vIVCg!0Dx9vYXATM diff --git a/demos/supabase-todolist-new-attachment/web/icons/Icon-192.png b/demos/supabase-todolist-new-attachment/web/icons/Icon-192.png deleted file mode 100644 index b749bfef07473333cf1dd31e9eed89862a5d52aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5292 zcmZ`-2T+sGz6~)*FVZ`aW+(v>MIm&M-g^@e2u-B-DoB?qO+b1Tq<5uCCv>ESfRum& zp%X;f!~1{tzL__3=gjVJ=j=J>+nMj%ncXj1Q(b|Ckbw{Y0FWpt%4y%$uD=Z*c-x~o zE;IoE;xa#7Ll5nj-e4CuXB&G*IM~D21rCP$*xLXAK8rIMCSHuSu%bL&S3)8YI~vyp@KBu9Ph7R_pvKQ@xv>NQ`dZp(u{Z8K3yOB zn7-AR+d2JkW)KiGx0hosml;+eCXp6+w%@STjFY*CJ?udJ64&{BCbuebcuH;}(($@@ znNlgBA@ZXB)mcl9nbX#F!f_5Z=W>0kh|UVWnf!At4V*LQP%*gPdCXd6P@J4Td;!Ur z<2ZLmwr(NG`u#gDEMP19UcSzRTL@HsK+PnIXbVBT@oHm53DZr?~V(0{rsalAfwgo zEh=GviaqkF;}F_5-yA!1u3!gxaR&Mj)hLuj5Q-N-@Lra{%<4ONja8pycD90&>yMB` zchhd>0CsH`^|&TstH-8+R`CfoWqmTTF_0?zDOY`E`b)cVi!$4xA@oO;SyOjJyP^_j zx^@Gdf+w|FW@DMdOi8=4+LJl$#@R&&=UM`)G!y%6ZzQLoSL%*KE8IO0~&5XYR9 z&N)?goEiWA(YoRfT{06&D6Yuu@Qt&XVbuW@COb;>SP9~aRc+z`m`80pB2o%`#{xD@ zI3RAlukL5L>px6b?QW1Ac_0>ew%NM!XB2(H+1Y3AJC?C?O`GGs`331Nd4ZvG~bMo{lh~GeL zSL|tT*fF-HXxXYtfu5z+T5Mx9OdP7J4g%@oeC2FaWO1D{=NvL|DNZ}GO?O3`+H*SI z=grGv=7dL{+oY0eJFGO!Qe(e2F?CHW(i!!XkGo2tUvsQ)I9ev`H&=;`N%Z{L zO?vV%rDv$y(@1Yj@xfr7Kzr<~0{^T8wM80xf7IGQF_S-2c0)0D6b0~yD7BsCy+(zL z#N~%&e4iAwi4F$&dI7x6cE|B{f@lY5epaDh=2-(4N05VO~A zQT3hanGy_&p+7Fb^I#ewGsjyCEUmSCaP6JDB*=_()FgQ(-pZ28-{qx~2foO4%pM9e z*_63RT8XjgiaWY|*xydf;8MKLd{HnfZ2kM%iq}fstImB-K6A79B~YoPVa@tYN@T_$ zea+9)<%?=Fl!kd(Y!G(-o}ko28hg2!MR-o5BEa_72uj7Mrc&{lRh3u2%Y=Xk9^-qa zBPWaD=2qcuJ&@Tf6ue&)4_V*45=zWk@Z}Q?f5)*z)-+E|-yC4fs5CE6L_PH3=zI8p z*Z3!it{1e5_^(sF*v=0{`U9C741&lub89gdhKp|Y8CeC{_{wYK-LSbp{h)b~9^j!s z7e?Y{Z3pZv0J)(VL=g>l;<}xk=T*O5YR|hg0eg4u98f2IrA-MY+StQIuK-(*J6TRR z|IM(%uI~?`wsfyO6Tgmsy1b3a)j6M&-jgUjVg+mP*oTKdHg?5E`!r`7AE_#?Fc)&a z08KCq>Gc=ne{PCbRvs6gVW|tKdcE1#7C4e`M|j$C5EYZ~Y=jUtc zj`+?p4ba3uy7><7wIokM79jPza``{Lx0)zGWg;FW1^NKY+GpEi=rHJ+fVRGfXO zPHV52k?jxei_!YYAw1HIz}y8ZMwdZqU%ESwMn7~t zdI5%B;U7RF=jzRz^NuY9nM)&<%M>x>0(e$GpU9th%rHiZsIT>_qp%V~ILlyt^V`=d z!1+DX@ah?RnB$X!0xpTA0}lN@9V-ePx>wQ?-xrJr^qDlw?#O(RsXeAvM%}rg0NT#t z!CsT;-vB=B87ShG`GwO;OEbeL;a}LIu=&@9cb~Rsx(ZPNQ!NT7H{@j0e(DiLea>QD zPmpe90gEKHEZ8oQ@6%E7k-Ptn#z)b9NbD@_GTxEhbS+}Bb74WUaRy{w;E|MgDAvHw zL)ycgM7mB?XVh^OzbC?LKFMotw3r@i&VdUV%^Efdib)3@soX%vWCbnOyt@Y4swW925@bt45y0HY3YI~BnnzZYrinFy;L?2D3BAL`UQ zEj))+f>H7~g8*VuWQ83EtGcx`hun$QvuurSMg3l4IP8Fe`#C|N6mbYJ=n;+}EQm;< z!!N=5j1aAr_uEnnzrEV%_E|JpTb#1p1*}5!Ce!R@d$EtMR~%9# zd;h8=QGT)KMW2IKu_fA_>p_und#-;Q)p%%l0XZOXQicfX8M~7?8}@U^ihu;mizj)t zgV7wk%n-UOb z#!P5q?Ex+*Kx@*p`o$q8FWL*E^$&1*!gpv?Za$YO~{BHeGY*5%4HXUKa_A~~^d z=E*gf6&+LFF^`j4$T~dR)%{I)T?>@Ma?D!gi9I^HqvjPc3-v~=qpX1Mne@*rzT&Xw zQ9DXsSV@PqpEJO-g4A&L{F&;K6W60D!_vs?Vx!?w27XbEuJJP&);)^+VF1nHqHBWu z^>kI$M9yfOY8~|hZ9WB!q-9u&mKhEcRjlf2nm_@s;0D#c|@ED7NZE% zzR;>P5B{o4fzlfsn3CkBK&`OSb-YNrqx@N#4CK!>bQ(V(D#9|l!e9(%sz~PYk@8zt zPN9oK78&-IL_F zhsk1$6p;GqFbtB^ZHHP+cjMvA0(LqlskbdYE_rda>gvQLTiqOQ1~*7lg%z*&p`Ry& zRcG^DbbPj_jOKHTr8uk^15Boj6>hA2S-QY(W-6!FIq8h$<>MI>PYYRenQDBamO#Fv zAH5&ImqKBDn0v5kb|8i0wFhUBJTpT!rB-`zK)^SNnRmLraZcPYK7b{I@+}wXVdW-{Ps17qdRA3JatEd?rPV z4@}(DAMf5EqXCr4-B+~H1P#;t@O}B)tIJ(W6$LrK&0plTmnPpb1TKn3?f?Kk``?D+ zQ!MFqOX7JbsXfQrz`-M@hq7xlfNz;_B{^wbpG8des56x(Q)H)5eLeDwCrVR}hzr~= zM{yXR6IM?kXxauLza#@#u?Y|o;904HCqF<8yT~~c-xyRc0-vxofnxG^(x%>bj5r}N zyFT+xnn-?B`ohA>{+ZZQem=*Xpqz{=j8i2TAC#x-m;;mo{{sLB_z(UoAqD=A#*juZ zCv=J~i*O8;F}A^Wf#+zx;~3B{57xtoxC&j^ie^?**T`WT2OPRtC`xj~+3Kprn=rVM zVJ|h5ux%S{dO}!mq93}P+h36mZ5aZg1-?vhL$ke1d52qIiXSE(llCr5i=QUS?LIjc zV$4q=-)aaR4wsrQv}^shL5u%6;`uiSEs<1nG^?$kl$^6DL z43CjY`M*p}ew}}3rXc7Xck@k41jx}c;NgEIhKZ*jsBRZUP-x2cm;F1<5$jefl|ppO zmZd%%?gMJ^g9=RZ^#8Mf5aWNVhjAS^|DQO+q$)oeob_&ZLFL(zur$)); zU19yRm)z<4&4-M}7!9+^Wl}Uk?`S$#V2%pQ*SIH5KI-mn%i;Z7-)m$mN9CnI$G7?# zo`zVrUwoSL&_dJ92YhX5TKqaRkfPgC4=Q&=K+;_aDs&OU0&{WFH}kKX6uNQC6%oUH z2DZa1s3%Vtk|bglbxep-w)PbFG!J17`<$g8lVhqD2w;Z0zGsh-r zxZ13G$G<48leNqR!DCVt9)@}(zMI5w6Wo=N zpP1*3DI;~h2WDWgcKn*f!+ORD)f$DZFwgKBafEZmeXQMAsq9sxP9A)7zOYnkHT9JU zRA`umgmP9d6=PHmFIgx=0$(sjb>+0CHG)K@cPG{IxaJ&Ueo8)0RWgV9+gO7+Bl1(F z7!BslJ2MP*PWJ;x)QXbR$6jEr5q3 z(3}F@YO_P1NyTdEXRLU6fp?9V2-S=E+YaeLL{Y)W%6`k7$(EW8EZSA*(+;e5@jgD^I zaJQ2|oCM1n!A&-8`;#RDcZyk*+RPkn_r8?Ak@agHiSp*qFNX)&i21HE?yuZ;-C<3C zwJGd1lx5UzViP7sZJ&|LqH*mryb}y|%AOw+v)yc`qM)03qyyrqhX?ub`Cjwx2PrR! z)_z>5*!*$x1=Qa-0uE7jy0z`>|Ni#X+uV|%_81F7)b+nf%iz=`fF4g5UfHS_?PHbr zB;0$bK@=di?f`dS(j{l3-tSCfp~zUuva+=EWxJcRfp(<$@vd(GigM&~vaYZ0c#BTs z3ijkxMl=vw5AS&DcXQ%eeKt!uKvh2l3W?&3=dBHU=Gz?O!40S&&~ei2vg**c$o;i89~6DVns zG>9a*`k5)NI9|?W!@9>rzJ;9EJ=YlJTx1r1BA?H`LWijk(rTax9(OAu;q4_wTj-yj z1%W4GW&K4T=uEGb+E!>W0SD_C0RR91 diff --git a/demos/supabase-todolist-new-attachment/web/icons/Icon-512.png b/demos/supabase-todolist-new-attachment/web/icons/Icon-512.png deleted file mode 100644 index 88cfd48dff1169879ba46840804b412fe02fefd6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8252 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 diff --git a/demos/supabase-todolist-new-attachment/web/icons/Icon-maskable-192.png b/demos/supabase-todolist-new-attachment/web/icons/Icon-maskable-192.png deleted file mode 100644 index eb9b4d76e525556d5d89141648c724331630325d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5594 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 X>c!)3PgKfL&lX^$Z>Cpu&6)6jvi^Z! diff --git a/demos/supabase-todolist-new-attachment/web/icons/Icon-maskable-512.png b/demos/supabase-todolist-new-attachment/web/icons/Icon-maskable-512.png deleted file mode 100644 index d69c56691fbdb0b7efa65097c7cc1edac12a6d3e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20998 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 MPgg&ebxsLQ025IeI{*Lx diff --git a/demos/supabase-todolist-new-attachment/web/index.html b/demos/supabase-todolist-new-attachment/web/index.html deleted file mode 100644 index b3b0c490..00000000 --- a/demos/supabase-todolist-new-attachment/web/index.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - supabase_todolist - - - - - - - - - - diff --git a/demos/supabase-todolist-new-attachment/web/manifest.json b/demos/supabase-todolist-new-attachment/web/manifest.json deleted file mode 100644 index 9dcf6fe4..00000000 --- a/demos/supabase-todolist-new-attachment/web/manifest.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "supabase_todolist", - "short_name": "supabase_todolist", - "start_url": ".", - "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", - "description": "A new Flutter project.", - "orientation": "portrait-primary", - "prefer_related_applications": false, - "icons": [ - { - "src": "icons/Icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "icons/Icon-512.png", - "sizes": "512x512", - "type": "image/png" - }, - { - "src": "icons/Icon-maskable-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "icons/Icon-maskable-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ] -} diff --git a/demos/supabase-todolist-new-attachment/windows/.gitignore b/demos/supabase-todolist-new-attachment/windows/.gitignore deleted file mode 100644 index d492d0d9..00000000 --- a/demos/supabase-todolist-new-attachment/windows/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -flutter/ephemeral/ - -# Visual Studio user-specific files. -*.suo -*.user -*.userosscache -*.sln.docstates - -# Visual Studio build-related files. -x64/ -x86/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ diff --git a/demos/supabase-todolist-new-attachment/windows/CMakeLists.txt b/demos/supabase-todolist-new-attachment/windows/CMakeLists.txt deleted file mode 100644 index ccfc4498..00000000 --- a/demos/supabase-todolist-new-attachment/windows/CMakeLists.txt +++ /dev/null @@ -1,101 +0,0 @@ -# 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/supabase-todolist-new-attachment/windows/flutter/CMakeLists.txt b/demos/supabase-todolist-new-attachment/windows/flutter/CMakeLists.txt deleted file mode 100644 index 930d2071..00000000 --- a/demos/supabase-todolist-new-attachment/windows/flutter/CMakeLists.txt +++ /dev/null @@ -1,104 +0,0 @@ -# This file controls Flutter-level build steps. It should not be edited. -cmake_minimum_required(VERSION 3.14) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. -set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") - -# === Flutter Library === -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "flutter_export.h" - "flutter_windows.h" - "flutter_messenger.h" - "flutter_plugin_registrar.h" - "flutter_texture_registrar.h" -) -list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") -add_dependencies(flutter flutter_assemble) - -# === Wrapper === -list(APPEND CPP_WRAPPER_SOURCES_CORE - "core_implementations.cc" - "standard_codec.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") -list(APPEND CPP_WRAPPER_SOURCES_PLUGIN - "plugin_registrar.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") -list(APPEND CPP_WRAPPER_SOURCES_APP - "flutter_engine.cc" - "flutter_view_controller.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") - -# Wrapper sources needed for a plugin. -add_library(flutter_wrapper_plugin STATIC - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_PLUGIN} -) -apply_standard_settings(flutter_wrapper_plugin) -set_target_properties(flutter_wrapper_plugin PROPERTIES - POSITION_INDEPENDENT_CODE ON) -set_target_properties(flutter_wrapper_plugin PROPERTIES - CXX_VISIBILITY_PRESET hidden) -target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) -target_include_directories(flutter_wrapper_plugin PUBLIC - "${WRAPPER_ROOT}/include" -) -add_dependencies(flutter_wrapper_plugin flutter_assemble) - -# Wrapper sources needed for the runner. -add_library(flutter_wrapper_app STATIC - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_APP} -) -apply_standard_settings(flutter_wrapper_app) -target_link_libraries(flutter_wrapper_app PUBLIC flutter) -target_include_directories(flutter_wrapper_app PUBLIC - "${WRAPPER_ROOT}/include" -) -add_dependencies(flutter_wrapper_app flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") -set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} - ${CPP_WRAPPER_SOURCES_APP} - ${PHONY_OUTPUT} - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_PLUGIN} - ${CPP_WRAPPER_SOURCES_APP} -) diff --git a/demos/supabase-todolist-new-attachment/windows/flutter/generated_plugin_registrant.cc b/demos/supabase-todolist-new-attachment/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 691e6fc2..00000000 --- a/demos/supabase-todolist-new-attachment/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,23 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#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( - registry->GetRegistrarForPlugin("UrlLauncherWindows")); -} diff --git a/demos/supabase-todolist-new-attachment/windows/flutter/generated_plugin_registrant.h b/demos/supabase-todolist-new-attachment/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d85..00000000 --- a/demos/supabase-todolist-new-attachment/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/demos/supabase-todolist-new-attachment/windows/flutter/generated_plugins.cmake b/demos/supabase-todolist-new-attachment/windows/flutter/generated_plugins.cmake deleted file mode 100644 index 0d5e9159..00000000 --- a/demos/supabase-todolist-new-attachment/windows/flutter/generated_plugins.cmake +++ /dev/null @@ -1,27 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST - app_links - powersync_flutter_libs - sqlite3_flutter_libs - url_launcher_windows -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/demos/supabase-todolist-new-attachment/windows/runner/CMakeLists.txt b/demos/supabase-todolist-new-attachment/windows/runner/CMakeLists.txt deleted file mode 100644 index 394917c0..00000000 --- a/demos/supabase-todolist-new-attachment/windows/runner/CMakeLists.txt +++ /dev/null @@ -1,40 +0,0 @@ -cmake_minimum_required(VERSION 3.14) -project(runner LANGUAGES CXX) - -# Define the application target. To change its name, change BINARY_NAME in the -# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer -# work. -# -# Any new source files that you add to the application should be added here. -add_executable(${BINARY_NAME} WIN32 - "flutter_window.cpp" - "main.cpp" - "utils.cpp" - "win32_window.cpp" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" - "Runner.rc" - "runner.exe.manifest" -) - -# Apply the standard set of build settings. This can be removed for applications -# that need different build settings. -apply_standard_settings(${BINARY_NAME}) - -# Add preprocessor definitions for the build version. -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") - -# Disable Windows macros that collide with C++ standard library functions. -target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") - -# Add dependency libraries and include directories. Add any application-specific -# dependencies here. -target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) -target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") -target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") - -# Run the Flutter tool portions of the build. This must not be removed. -add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/demos/supabase-todolist-new-attachment/windows/runner/Runner.rc b/demos/supabase-todolist-new-attachment/windows/runner/Runner.rc deleted file mode 100644 index 75674c07..00000000 --- a/demos/supabase-todolist-new-attachment/windows/runner/Runner.rc +++ /dev/null @@ -1,121 +0,0 @@ -// 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/supabase-todolist-new-attachment/windows/runner/flutter_window.cpp b/demos/supabase-todolist-new-attachment/windows/runner/flutter_window.cpp deleted file mode 100644 index b25e363e..00000000 --- a/demos/supabase-todolist-new-attachment/windows/runner/flutter_window.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "flutter_window.h" - -#include - -#include "flutter/generated_plugin_registrant.h" - -FlutterWindow::FlutterWindow(const flutter::DartProject& project) - : project_(project) {} - -FlutterWindow::~FlutterWindow() {} - -bool FlutterWindow::OnCreate() { - if (!Win32Window::OnCreate()) { - return false; - } - - RECT frame = GetClientArea(); - - // The size here must match the window dimensions to avoid unnecessary surface - // creation / destruction in the startup path. - flutter_controller_ = std::make_unique( - frame.right - frame.left, frame.bottom - frame.top, project_); - // Ensure that basic setup of the controller was successful. - if (!flutter_controller_->engine() || !flutter_controller_->view()) { - return false; - } - RegisterPlugins(flutter_controller_->engine()); - SetChildContent(flutter_controller_->view()->GetNativeWindow()); - - flutter_controller_->engine()->SetNextFrameCallback([&]() { - this->Show(); - }); - - return true; -} - -void FlutterWindow::OnDestroy() { - if (flutter_controller_) { - flutter_controller_ = nullptr; - } - - Win32Window::OnDestroy(); -} - -LRESULT -FlutterWindow::MessageHandler(HWND hwnd, UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - // Give Flutter, including plugins, an opportunity to handle window messages. - if (flutter_controller_) { - std::optional result = - flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, - lparam); - if (result) { - return *result; - } - } - - switch (message) { - case WM_FONTCHANGE: - flutter_controller_->engine()->ReloadSystemFonts(); - break; - } - - return Win32Window::MessageHandler(hwnd, message, wparam, lparam); -} diff --git a/demos/supabase-todolist-new-attachment/windows/runner/flutter_window.h b/demos/supabase-todolist-new-attachment/windows/runner/flutter_window.h deleted file mode 100644 index 6da0652f..00000000 --- a/demos/supabase-todolist-new-attachment/windows/runner/flutter_window.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef RUNNER_FLUTTER_WINDOW_H_ -#define RUNNER_FLUTTER_WINDOW_H_ - -#include -#include - -#include - -#include "win32_window.h" - -// A window that does nothing but host a Flutter view. -class FlutterWindow : public Win32Window { - public: - // Creates a new FlutterWindow hosting a Flutter view running |project|. - explicit FlutterWindow(const flutter::DartProject& project); - virtual ~FlutterWindow(); - - protected: - // Win32Window: - bool OnCreate() override; - void OnDestroy() override; - LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, - LPARAM const lparam) noexcept override; - - private: - // The project to run. - flutter::DartProject project_; - - // The Flutter instance hosted by this window. - std::unique_ptr flutter_controller_; -}; - -#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/demos/supabase-todolist-new-attachment/windows/runner/main.cpp b/demos/supabase-todolist-new-attachment/windows/runner/main.cpp deleted file mode 100644 index 3eee96b7..00000000 --- a/demos/supabase-todolist-new-attachment/windows/runner/main.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#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/supabase-todolist-new-attachment/windows/runner/resource.h b/demos/supabase-todolist-new-attachment/windows/runner/resource.h deleted file mode 100644 index 66a65d1e..00000000 --- a/demos/supabase-todolist-new-attachment/windows/runner/resource.h +++ /dev/null @@ -1,16 +0,0 @@ -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by Runner.rc -// -#define IDI_APP_ICON 101 - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -#ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 102 -#define _APS_NEXT_COMMAND_VALUE 40001 -#define _APS_NEXT_CONTROL_VALUE 1001 -#define _APS_NEXT_SYMED_VALUE 101 -#endif -#endif diff --git a/demos/supabase-todolist-new-attachment/windows/runner/resources/app_icon.ico b/demos/supabase-todolist-new-attachment/windows/runner/resources/app_icon.ico deleted file mode 100644 index c04e20caf6370ebb9253ad831cc31de4a9c965f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33772 zcmeHQc|26z|35SKE&G-*mXah&B~fFkXr)DEO&hIfqby^T&>|8^_Ub8Vp#`BLl3lbZ zvPO!8k!2X>cg~Elr=IVxo~J*a`+9wR=A83c-k-DFd(XM&UI1VKCqM@V;DDtJ09WB} zRaHKiW(GT00brH|0EeTeKVbpbGZg?nK6-j827q-+NFM34gXjqWxJ*a#{b_apGN<-L_m3#8Z26atkEn& ze87Bvv^6vVmM+p+cQ~{u%=NJF>#(d;8{7Q{^rWKWNtf14H}>#&y7$lqmY6xmZryI& z($uy?c5-+cPnt2%)R&(KIWEXww>Cnz{OUpT>W$CbO$h1= z#4BPMkFG1Y)x}Ui+WXr?Z!w!t_hjRq8qTaWpu}FH{MsHlU{>;08goVLm{V<&`itk~ zE_Ys=D(hjiy+5=?=$HGii=Y5)jMe9|wWoD_K07(}edAxh`~LBorOJ!Cf@f{_gNCC| z%{*04ViE!#>@hc1t5bb+NO>ncf@@Dv01K!NxH$3Eg1%)|wLyMDF8^d44lV!_Sr}iEWefOaL z8f?ud3Q%Sen39u|%00W<#!E=-RpGa+H8}{ulxVl4mwpjaU+%2pzmi{3HM)%8vb*~-M9rPUAfGCSos8GUXp02|o~0BTV2l#`>>aFV&_P$ejS;nGwSVP8 zMbOaG7<7eKD>c12VdGH;?2@q7535sa7MN*L@&!m?L`ASG%boY7(&L5imY#EQ$KrBB z4@_tfP5m50(T--qv1BJcD&aiH#b-QC>8#7Fx@3yXlonJI#aEIi=8&ChiVpc#N=5le zM*?rDIdcpawoc5kizv$GEjnveyrp3sY>+5_R5;>`>erS%JolimF=A^EIsAK zsPoVyyUHCgf0aYr&alx`<)eb6Be$m&`JYSuBu=p8j%QlNNp$-5C{b4#RubPb|CAIS zGE=9OFLP7?Hgc{?k45)84biT0k&-C6C%Q}aI~q<(7BL`C#<6HyxaR%!dFx7*o^laG z=!GBF^cwK$IA(sn9y6>60Rw{mYRYkp%$jH z*xQM~+bp)G$_RhtFPYx2HTsWk80+p(uqv9@I9)y{b$7NK53rYL$ezbmRjdXS?V}fj zWxX_feWoLFNm3MG7pMUuFPs$qrQWO9!l2B(SIuy2}S|lHNbHzoE+M2|Zxhjq9+Ws8c{*}x^VAib7SbxJ*Q3EnY5lgI9 z=U^f3IW6T=TWaVj+2N%K3<%Un;CF(wUp`TC&Y|ZjyFu6co^uqDDB#EP?DV5v_dw~E zIRK*BoY9y-G_ToU2V_XCX4nJ32~`czdjT!zwme zGgJ0nOk3U4@IE5JwtM}pwimLjk{ln^*4HMU%Fl4~n(cnsLB}Ja-jUM>xIB%aY;Nq8 z)Fp8dv1tkqKanv<68o@cN|%thj$+f;zGSO7H#b+eMAV8xH$hLggtt?O?;oYEgbq@= zV(u9bbd12^%;?nyk6&$GPI%|+<_mEpJGNfl*`!KV;VfmZWw{n{rnZ51?}FDh8we_L z8OI9nE31skDqJ5Oa_ybn7|5@ui>aC`s34p4ZEu6-s!%{uU45$Zd1=p$^^dZBh zu<*pDDPLW+c>iWO$&Z_*{VSQKg7=YEpS3PssPn1U!lSm6eZIho*{@&20e4Y_lRklKDTUCKI%o4Pc<|G^Xgu$J^Q|B87U;`c1zGwf^-zH*VQ^x+i^OUWE0yd z;{FJq)2w!%`x7yg@>uGFFf-XJl4H`YtUG%0slGKOlXV`q?RP>AEWg#x!b{0RicxGhS!3$p7 zij;{gm!_u@D4$Ox%>>bPtLJ> zwKtYz?T_DR1jN>DkkfGU^<#6sGz|~p*I{y`aZ>^Di#TC|Z!7j_O1=Wo8thuit?WxR zh9_S>kw^{V^|g}HRUF=dcq>?q(pHxw!8rx4dC6vbQVmIhmICF#zU!HkHpQ>9S%Uo( zMw{eC+`&pb=GZRou|3;Po1}m46H6NGd$t<2mQh}kaK-WFfmj_66_17BX0|j-E2fe3Jat}ijpc53 zJV$$;PC<5aW`{*^Z6e5##^`Ed#a0nwJDT#Qq~^e8^JTA=z^Kl>La|(UQ!bI@#ge{Dzz@61p-I)kc2?ZxFt^QQ}f%ldLjO*GPj(5)V9IyuUakJX=~GnTgZ4$5!3E=V#t`yOG4U z(gphZB6u2zsj=qNFLYShhg$}lNpO`P9xOSnO*$@@UdMYES*{jJVj|9z-}F^riksLK zbsU+4-{281P9e2UjY6tse^&a)WM1MFw;p#_dHhWI7p&U*9TR0zKdVuQed%6{otTsq z$f~S!;wg#Bd9kez=Br{m|66Wv z#g1xMup<0)H;c2ZO6su_ii&m8j&+jJz4iKnGZ&wxoQX|5a>v&_e#6WA!MB_4asTxLRGQCC5cI(em z%$ZfeqP>!*q5kU>a+BO&ln=4Jm>Ef(QE8o&RgLkk%2}4Tf}U%IFP&uS7}&|Q-)`5< z+e>;s#4cJ-z%&-^&!xsYx777Wt(wZY9(3(avmr|gRe4cD+a8&!LY`1^T?7x{E<=kdY9NYw>A;FtTvQ=Y&1M%lyZPl$ss1oY^Sl8we}n}Aob#6 zl4jERwnt9BlSoWb@3HxYgga(752Vu6Y)k4yk9u~Kw>cA5&LHcrvn1Y-HoIuFWg~}4 zEw4bR`mXZQIyOAzo)FYqg?$5W<;^+XX%Uz61{-L6@eP|lLH%|w?g=rFc;OvEW;^qh z&iYXGhVt(G-q<+_j}CTbPS_=K>RKN0&;dubh0NxJyDOHFF;<1k!{k#7b{|Qok9hac z;gHz}6>H6C6RnB`Tt#oaSrX0p-j-oRJ;_WvS-qS--P*8}V943RT6kou-G=A+7QPGQ z!ze^UGxtW3FC0$|(lY9^L!Lx^?Q8cny(rR`es5U;-xBhphF%_WNu|aO<+e9%6LuZq zt(0PoagJG<%hyuf;te}n+qIl_Ej;czWdc{LX^pS>77s9t*2b4s5dvP_!L^3cwlc)E!(!kGrg~FescVT zZCLeua3f4;d;Tk4iXzt}g}O@nlK3?_o91_~@UMIl?@77Qc$IAlLE95#Z=TES>2E%z zxUKpK{_HvGF;5%Q7n&vA?`{%8ohlYT_?(3A$cZSi)MvIJygXD}TS-3UwyUxGLGiJP znblO~G|*uA^|ac8E-w#}uBtg|s_~s&t>-g0X%zIZ@;o_wNMr_;{KDg^O=rg`fhDZu zFp(VKd1Edj%F zWHPl+)FGj%J1BO3bOHVfH^3d1F{)*PL&sRX`~(-Zy3&9UQX)Z;c51tvaI2E*E7!)q zcz|{vpK7bjxix(k&6=OEIBJC!9lTkUbgg?4-yE{9+pFS)$Ar@vrIf`D0Bnsed(Cf? zObt2CJ>BKOl>q8PyFO6w)+6Iz`LW%T5^R`U_NIW0r1dWv6OY=TVF?N=EfA(k(~7VBW(S;Tu5m4Lg8emDG-(mOSSs=M9Q&N8jc^Y4&9RqIsk(yO_P(mcCr}rCs%1MW1VBrn=0-oQN(Xj!k%iKV zb%ricBF3G4S1;+8lzg5PbZ|$Se$)I=PwiK=cDpHYdov2QO1_a-*dL4KUi|g&oh>(* zq$<`dQ^fat`+VW?m)?_KLn&mp^-@d=&7yGDt<=XwZZC=1scwxO2^RRI7n@g-1o8ps z)&+et_~)vr8aIF1VY1Qrq~Xe``KJrQSnAZ{CSq3yP;V*JC;mmCT6oRLSs7=GA?@6g zUooM}@tKtx(^|aKK8vbaHlUQqwE0}>j&~YlN3H#vKGm@u)xxS?n9XrOWUfCRa< z`20Fld2f&;gg7zpo{Adh+mqNntMc-D$N^yWZAZRI+u1T1zWHPxk{+?vcS1D>08>@6 zLhE@`gt1Y9mAK6Z4p|u(5I%EkfU7rKFSM=E4?VG9tI;a*@?6!ey{lzN5=Y-!$WFSe z&2dtO>^0@V4WRc#L&P%R(?@KfSblMS+N+?xUN$u3K4Ys%OmEh+tq}fnU}i>6YHM?< zlnL2gl~sF!j!Y4E;j3eIU-lfa`RsOL*Tt<%EFC0gPzoHfNWAfKFIKZN8}w~(Yi~=q z>=VNLO2|CjkxP}RkutxjV#4fWYR1KNrPYq5ha9Wl+u>ipsk*I(HS@iLnmGH9MFlTU zaFZ*KSR0px>o+pL7BbhB2EC1%PJ{67_ z#kY&#O4@P=OV#-79y_W>Gv2dxL*@G7%LksNSqgId9v;2xJ zrh8uR!F-eU$NMx@S*+sk=C~Dxr9Qn7TfWnTupuHKuQ$;gGiBcU>GF5sWx(~4IP3`f zWE;YFO*?jGwYh%C3X<>RKHC-DZ!*r;cIr}GLOno^3U4tFSSoJp%oHPiSa%nh=Zgn% z14+8v@ygy0>UgEN1bczD6wK45%M>psM)y^)IfG*>3ItX|TzV*0i%@>L(VN!zdKb8S?Qf7BhjNpziA zR}?={-eu>9JDcl*R=OP9B8N$IcCETXah9SUDhr{yrld{G;PnCWRsPD7!eOOFBTWUQ=LrA_~)mFf&!zJX!Oc-_=kT<}m|K52 z)M=G#;p;Rdb@~h5D{q^K;^fX-m5V}L%!wVC2iZ1uu401Ll}#rocTeK|7FAeBRhNdQ zCc2d^aQnQp=MpOmak60N$OgS}a;p(l9CL`o4r(e-nN}mQ?M&isv-P&d$!8|1D1I(3-z!wi zTgoo)*Mv`gC?~bm?S|@}I|m-E2yqPEvYybiD5azInexpK8?9q*$9Yy9-t%5jU8~ym zgZDx>!@ujQ=|HJnwp^wv-FdD{RtzO9SnyfB{mH_(c!jHL*$>0o-(h(eqe*ZwF6Lvu z{7rkk%PEqaA>o+f{H02tzZ@TWy&su?VNw43! z-X+rN`6llvpUms3ZiSt)JMeztB~>9{J8SPmYs&qohxdYFi!ra8KR$35Zp9oR)eFC4 zE;P31#3V)n`w$fZ|4X-|%MX`xZDM~gJyl2W;O$H25*=+1S#%|53>|LyH za@yh+;325%Gq3;J&a)?%7X%t@WXcWL*BaaR*7UEZad4I8iDt7^R_Fd`XeUo256;sAo2F!HcIQKk;h})QxEsPE5BcKc7WyerTchgKmrfRX z!x#H_%cL#B9TWAqkA4I$R^8{%do3Y*&(;WFmJ zU7Dih{t1<{($VtJRl9|&EB?|cJ)xse!;}>6mSO$o5XIx@V|AA8ZcoD88ZM?C*;{|f zZVmf94_l1OmaICt`2sTyG!$^UeTHx9YuUP!omj(r|7zpm5475|yXI=rR>>fteLI+| z)MoiGho0oEt=*J(;?VY0QzwCqw@cVm?d7Y!z0A@u#H?sCJ*ecvyhj& z-F77lO;SH^dmf?L>3i>?Z*U}Em4ZYV_CjgfvzYsRZ+1B!Uo6H6mbS<-FFL`ytqvb& zE7+)2ahv-~dz(Hs+f})z{*4|{)b=2!RZK;PWwOnO=hG7xG`JU5>bAvUbdYd_CjvtHBHgtGdlO+s^9ca^Bv3`t@VRX2_AD$Ckg36OcQRF zXD6QtGfHdw*hx~V(MV-;;ZZF#dJ-piEF+s27z4X1qi5$!o~xBnvf=uopcn7ftfsZc zy@(PuOk`4GL_n(H9(E2)VUjqRCk9kR?w)v@xO6Jm_Mx})&WGEl=GS0#)0FAq^J*o! zAClhvoTsNP*-b~rN{8Yym3g{01}Ep^^Omf=SKqvN?{Q*C4HNNAcrowIa^mf+3PRy! z*_G-|3i8a;+q;iP@~Of_$(vtFkB8yOyWt2*K)vAn9El>=D;A$CEx6b*XF@4y_6M+2 zpeW`RHoI_p(B{%(&jTHI->hmNmZjHUj<@;7w0mx3&koy!2$@cfX{sN19Y}euYJFn& z1?)+?HCkD0MRI$~uB2UWri})0bru_B;klFdwsLc!ne4YUE;t41JqfG# zZJq6%vbsdx!wYeE<~?>o4V`A3?lN%MnKQ`z=uUivQN^vzJ|C;sdQ37Qn?;lpzg})y z)_2~rUdH}zNwX;Tp0tJ78+&I=IwOQ-fl30R79O8@?Ub8IIA(6I`yHn%lARVL`%b8+ z4$8D-|MZZWxc_)vu6@VZN!HsI$*2NOV&uMxBNzIbRgy%ob_ zhwEH{J9r$!dEix9XM7n&c{S(h>nGm?el;gaX0@|QnzFD@bne`el^CO$yXC?BDJ|Qg z+y$GRoR`?ST1z^e*>;!IS@5Ovb7*RlN>BV_UC!7E_F;N#ky%1J{+iixp(dUJj93aK zzHNN>R-oN7>kykHClPnoPTIj7zc6KM(Pnlb(|s??)SMb)4!sMHU^-ntJwY5Big7xv zb1Ew`Xj;|D2kzGja*C$eS44(d&RMU~c_Y14V9_TLTz0J#uHlsx`S6{nhsA0dWZ#cG zJ?`fO50E>*X4TQLv#nl%3GOk*UkAgt=IY+u0LNXqeln3Z zv$~&Li`ZJOKkFuS)dJRA>)b_Da%Q~axwA_8zNK{BH{#}#m}zGcuckz}riDE-z_Ms> zR8-EqAMcfyGJCtvTpaUVQtajhUS%c@Yj}&6Zz;-M7MZzqv3kA7{SuW$oW#=0az2wQ zg-WG@Vb4|D`pl~Il54N7Hmsauc_ne-a!o5#j3WaBBh@Wuefb!QJIOn5;d)%A#s+5% zuD$H=VNux9bE-}1&bcYGZ+>1Fo;3Z@e&zX^n!?JK*adSbONm$XW9z;Q^L>9U!}Toj2WdafJ%oL#h|yWWwyAGxzfrAWdDTtaKl zK4`5tDpPg5>z$MNv=X0LZ0d6l%D{(D8oT@+w0?ce$DZ6pv>{1&Ok67Ix1 zH}3=IEhPJEhItCC8E=`T`N5(k?G=B4+xzZ?<4!~ ze~z6Wk9!CHTI(0rLJ4{JU?E-puc;xusR?>G?;4vt;q~iI9=kDL=z0Rr%O$vU`30X$ zDZRFyZ`(omOy@u|i6h;wtJlP;+}$|Ak|k2dea7n?U1*$T!sXqqOjq^NxLPMmk~&qI zYg0W?yK8T(6+Ea+$YyspKK?kP$+B`~t3^Pib_`!6xCs32!i@pqXfFV6PmBIR<-QW= zN8L{pt0Vap0x`Gzn#E@zh@H)0FfVfA_Iu4fjYZ+umO1LXIbVc$pY+E234u)ttcrl$ z>s92z4vT%n6cMb>=XT6;l0+9e(|CZG)$@C7t7Z7Ez@a)h)!hyuV&B5K%%)P5?Lk|C zZZSVzdXp{@OXSP0hoU-gF8s8Um(#xzjP2Vem zec#-^JqTa&Y#QJ>-FBxd7tf`XB6e^JPUgagB8iBSEps;92KG`!#mvVcPQ5yNC-GEG zTiHEDYfH+0O15}r^+ z#jxj=@x8iNHWALe!P3R67TwmhItn**0JwnzSV2O&KE8KcT+0hWH^OPD1pwiuyx=b@ zNf5Jh0{9X)8;~Es)$t@%(3!OnbY+`@?i{mGX7Yy}8T_*0a6g;kaFPq;*=px5EhO{Cp%1kI<0?*|h8v!6WnO3cCJRF2-CRrU3JiLJnj@6;L)!0kWYAc_}F{2P))3HmCrz zQ&N&gE70;`!6*eJ4^1IR{f6j4(-l&X!tjHxkbHA^Zhrnhr9g{exN|xrS`5Pq=#Xf& zG%P=#ra-TyVFfgW%cZo5OSIwFL9WtXAlFOa+ubmI5t*3=g#Y zF%;70p5;{ZeFL}&}yOY1N1*Q;*<(kTB!7vM$QokF)yr2FlIU@$Ph58$Bz z0J?xQG=MlS4L6jA22eS42g|9*9pX@$#*sUeM(z+t?hr@r5J&D1rx}2pW&m*_`VDCW zUYY@v-;bAO0HqoAgbbiGGC<=ryf96}3pouhy3XJrX+!!u*O_>Si38V{uJmQ&USptX zKp#l(?>%^7;2%h(q@YWS#9;a!JhKlkR#Vd)ERILlgu!Hr@jA@V;sk4BJ-H#p*4EqC zDGjC*tl=@3Oi6)Bn^QwFpul18fpkbpg0+peH$xyPBqb%`$OUhPKyWb32o7clB*9Z< zN=i~NLjavrLtwgJ01bufP+>p-jR2I95|TpmKpQL2!oV>g(4RvS2pK4*ou%m(h6r3A zX#s&`9LU1ZG&;{CkOK!4fLDTnBys`M!vuz>Q&9OZ0hGQl!~!jSDg|~s*w52opC{sB ze|Cf2luD(*G13LcOAGA!s2FjSK8&IE5#W%J25w!vM0^VyQM!t)inj&RTiJ!wXzFgz z3^IqzB7I0L$llljsGq})thBy9UOyjtFO_*hYM_sgcMk>44jeH0V1FDyELc{S1F-;A zS;T^k^~4biG&V*Irq}O;e}j$$+E_#G?HKIn05iP3j|87TkGK~SqG!-KBg5+mN(aLm z8ybhIM`%C19UX$H$KY6JgXbY$0AT%rEpHC;u`rQ$Y=rxUdsc5*Kvc8jaYaO$^)cI6){P6K0r)I6DY4Wr4&B zLQUBraey#0HV|&c4v7PVo3n$zHj99(TZO^3?Ly%C4nYvJTL9eLBLHsM3WKKD>5!B` zQ=BsR3aR6PD(Fa>327E2HAu5TM~Wusc!)>~(gM)+3~m;92Jd;FnSib=M5d6;;5{%R zb4V7DEJ0V!CP-F*oU?gkc>ksUtAYP&V4ND5J>J2^jt*vcFflQWCrB&fLdT%O59PVJ zhid#toR=FNgD!q3&r8#wEBr`!wzvQu5zX?Q>nlSJ4i@WC*CN*-xU66F^V5crWevQ9gsq$I@z1o(a=k7LL~ z7m_~`o;_Ozha1$8Q}{WBehvAlO4EL60y5}8GDrZ< zXh&F}71JbW2A~8KfEWj&UWV#4+Z4p`b{uAj4&WC zha`}X@3~+Iz^WRlOHU&KngK>#j}+_o@LdBC1H-`gT+krWX3-;!)6?{FBp~%20a}FL zFP9%Emqcwa#(`=G>BBZ0qZDQhmZKJg_g8<=bBFKWr!dyg(YkpE+|R*SGpDVU!+VlU zFC54^DLv}`qa%49T>nNiA9Q7Ips#!Xx90tCU2gvK`(F+GPcL=J^>No{)~we#o@&mUb6c$ zCc*<|NJBk-#+{j9xkQ&ujB zI~`#kN~7W!f*-}wkG~Ld!JqZ@tK}eeSnsS5J1fMFXm|`LJx&}5`@dK3W^7#Wnm+_P zBZkp&j1fa2Y=eIjJ0}gh85jt43kaIXXv?xmo@eHrka!Z|vQv12HN#+!I5E z`(fbuW>gFiJL|uXJ!vKt#z3e3HlVdboH7;e#i3(2<)Fg-I@BR!qY#eof3MFZ&*Y@l zI|KJf&ge@p2Dq09Vu$$Qxb7!}{m-iRk@!)%KL)txi3;~Z4Pb}u@GsW;ELiWeG9V51 znX#}B&4Y2E7-H=OpNE@q{%hFLxwIpBF2t{vPREa8_{linXT;#1vMRWjOzLOP$-hf( z>=?$0;~~PnkqY;~K{EM6Vo-T(0K{A0}VUGmu*hR z{tw3hvBN%N3G3Yw`X5Te+F{J`(3w1s3-+1EbnFQKcrgrX1Jqvs@ADGe%M0s$EbK$$ zK)=y=upBc6SjGYAACCcI=Y*6Fi8_jgwZlLxD26fnQfJmb8^gHRN5(TemhX@0e=vr> zg`W}6U>x6VhoA3DqsGGD9uL1DhB3!OXO=k}59TqD@(0Nb{)Ut_luTioK_>7wjc!5C zIr@w}b`Fez3)0wQfKl&bae7;PcTA7%?f2xucM0G)wt_KO!Ewx>F~;=BI0j=Fb4>pp zv}0R^xM4eti~+^+gE$6b81p(kwzuDti(-K9bc|?+pJEl@H+jSYuxZQV8rl8 zjp@M{#%qItIUFN~KcO9Hed*`$5A-2~pAo~K&<-Q+`9`$CK>rzqAI4w~$F%vs9s{~x zg4BP%Gy*@m?;D6=SRX?888Q6peF@_4Z->8wAH~Cn!R$|Hhq2cIzFYqT_+cDourHbY z0qroxJnrZ4Gh+Ay+F`_c%+KRT>y3qw{)89?=hJ@=KO=@ep)aBJ$c!JHfBMJpsP*3G za7|)VJJ8B;4?n{~ldJF7%jmb`-ftIvNd~ekoufG(`K(3=LNc;HBY& z(lp#q8XAD#cIf}k49zX_i`*fO+#!zKA&%T3j@%)R+#yag067CU%yUEe47>wzGU8^` z1EXFT^@I!{J!F8!X?S6ph8J=gUi5tl93*W>7}_uR<2N2~e}FaG?}KPyugQ=-OGEZs z!GBoyYY+H*ANn4?Z)X4l+7H%`17i5~zRlRIX?t)6_eu=g2Q`3WBhxSUeea+M-S?RL zX9oBGKn%a!H+*hx4d2(I!gsi+@SQK%<{X22M~2tMulJoa)0*+z9=-YO+;DFEm5eE1U9b^B(Z}2^9!Qk`!A$wUE z7$Ar5?NRg2&G!AZqnmE64eh^Anss3i!{}%6@Et+4rr!=}!SBF8eZ2*J3ujCWbl;3; z48H~goPSv(8X61fKKdpP!Z7$88NL^Z?j`!^*I?-P4X^pMxyWz~@$(UeAcTSDd(`vO z{~rc;9|GfMJcApU3k}22a!&)k4{CU!e_ny^Y3cO;tOvOMKEyWz!vG(Kp*;hB?d|R3`2X~=5a6#^o5@qn?J-bI8Ppip{-yG z!k|VcGsq!jF~}7DMr49Wap-s&>o=U^T0!Lcy}!(bhtYsPQy z4|EJe{12QL#=c(suQ89Mhw9<`bui%nx7Nep`C&*M3~vMEACmcRYYRGtANq$F%zh&V zc)cEVeHz*Z1N)L7k-(k3np#{GcDh2Q@ya0YHl*n7fl*ZPAsbU-a94MYYtA#&!c`xGIaV;yzsmrjfieTEtqB_WgZp2*NplHx=$O{M~2#i_vJ{ps-NgK zQsxKK_CBM2PP_je+Xft`(vYfXXgIUr{=PA=7a8`2EHk)Ym2QKIforz# tySWtj{oF3N9@_;i*Fv5S)9x^z=nlWP>jpp-9)52ZmLVA=i*%6g{{fxOO~wEK diff --git a/demos/supabase-todolist-new-attachment/windows/runner/runner.exe.manifest b/demos/supabase-todolist-new-attachment/windows/runner/runner.exe.manifest deleted file mode 100644 index a42ea768..00000000 --- a/demos/supabase-todolist-new-attachment/windows/runner/runner.exe.manifest +++ /dev/null @@ -1,20 +0,0 @@ - - - - - PerMonitorV2 - - - - - - - - - - - - - - - diff --git a/demos/supabase-todolist-new-attachment/windows/runner/utils.cpp b/demos/supabase-todolist-new-attachment/windows/runner/utils.cpp deleted file mode 100644 index f5bf9fa0..00000000 --- a/demos/supabase-todolist-new-attachment/windows/runner/utils.cpp +++ /dev/null @@ -1,64 +0,0 @@ -#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/supabase-todolist-new-attachment/windows/runner/utils.h b/demos/supabase-todolist-new-attachment/windows/runner/utils.h deleted file mode 100644 index 3879d547..00000000 --- a/demos/supabase-todolist-new-attachment/windows/runner/utils.h +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef RUNNER_UTILS_H_ -#define RUNNER_UTILS_H_ - -#include -#include - -// Creates a console for the process, and redirects stdout and stderr to -// it for both the runner and the Flutter library. -void CreateAndAttachConsole(); - -// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string -// encoded in UTF-8. Returns an empty std::string on failure. -std::string Utf8FromUtf16(const wchar_t* utf16_string); - -// Gets the command line arguments passed in as a std::vector, -// encoded in UTF-8. Returns an empty std::vector on failure. -std::vector GetCommandLineArguments(); - -#endif // RUNNER_UTILS_H_ diff --git a/demos/supabase-todolist-new-attachment/windows/runner/win32_window.cpp b/demos/supabase-todolist-new-attachment/windows/runner/win32_window.cpp deleted file mode 100644 index 041a3855..00000000 --- a/demos/supabase-todolist-new-attachment/windows/runner/win32_window.cpp +++ /dev/null @@ -1,288 +0,0 @@ -#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/supabase-todolist-new-attachment/windows/runner/win32_window.h b/demos/supabase-todolist-new-attachment/windows/runner/win32_window.h deleted file mode 100644 index c86632d8..00000000 --- a/demos/supabase-todolist-new-attachment/windows/runner/win32_window.h +++ /dev/null @@ -1,102 +0,0 @@ -#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-todolist/lib/attachments/photo_capture_widget.dart b/demos/supabase-todolist/lib/attachments/photo_capture_widget.dart index 38838dd7..61d26ea6 100644 --- a/demos/supabase-todolist/lib/attachments/photo_capture_widget.dart +++ b/demos/supabase-todolist/lib/attachments/photo_capture_widget.dart @@ -1,18 +1,16 @@ import 'dart:async'; - +import 'dart:io'; +import 'dart:typed_data'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; -import 'package:powersync/powersync.dart' as powersync; +import 'package:logging/logging.dart'; import 'package:powersync_flutter_demo/attachments/queue.dart'; -import 'package:powersync_flutter_demo/models/todo_item.dart'; -import 'package:powersync_flutter_demo/powersync.dart'; class TakePhotoWidget extends StatefulWidget { final String todoId; final CameraDescription camera; - const TakePhotoWidget( - {super.key, required this.todoId, required this.camera}); + const TakePhotoWidget({super.key, required this.todoId, required this.camera}); @override State createState() { @@ -23,6 +21,7 @@ class TakePhotoWidget extends StatefulWidget { class _TakePhotoWidgetState extends State { late CameraController _cameraController; late Future _initializeControllerFuture; + final log = Logger('TakePhotoWidget'); @override void initState() { @@ -37,7 +36,6 @@ class _TakePhotoWidgetState extends State { } @override - // Dispose of the camera controller when the widget is disposed void dispose() { _cameraController.dispose(); super.dispose(); @@ -45,25 +43,26 @@ class _TakePhotoWidgetState extends State { Future _takePhoto(context) async { try { - // Ensure the camera is initialized before taking a photo + log.info('Taking photo for todo: ${widget.todoId}'); await _initializeControllerFuture; - final XFile photo = await _cameraController.takePicture(); - // copy photo to new directory with ID as name - String photoId = powersync.uuid.v4(); - String storageDirectory = await attachmentQueue.getStorageDirectory(); - await attachmentQueue.localStorage - .copyFile(photo.path, '$storageDirectory/$photoId.jpg'); - - int photoSize = await photo.length(); - - TodoItem.addPhoto(photoId, widget.todoId); - attachmentQueue.saveFile(photoId, photoSize); + + // Read the photo data as a stream + final photoFile = File(photo.path); + if (!await photoFile.exists()) { + log.warning('Photo file does not exist: ${photo.path}'); + return; + } + + final photoDataStream = photoFile.openRead().cast(); + + // Save the photo attachment directly with the data stream + final attachment = await savePhotoAttachment(photoDataStream, widget.todoId); + + log.info('Photo attachment saved with ID: ${attachment.id}'); } catch (e) { - log.info('Error taking photo: $e'); + log.severe('Error taking photo: $e'); } - - // After taking the photo, navigate back to the previous screen Navigator.pop(context); } diff --git a/demos/supabase-todolist/lib/attachments/photo_widget.dart b/demos/supabase-todolist/lib/attachments/photo_widget.dart index f034ef5b..9de3303a 100644 --- a/demos/supabase-todolist/lib/attachments/photo_widget.dart +++ b/demos/supabase-todolist/lib/attachments/photo_widget.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:powersync_attachments_helper/powersync_attachments_helper.dart'; +import 'package:powersync_attachments_stream/powersync_attachments_stream.dart'; import 'package:powersync_flutter_demo/attachments/camera_helpers.dart'; import 'package:powersync_flutter_demo/attachments/photo_capture_widget.dart'; import 'package:powersync_flutter_demo/attachments/queue.dart'; diff --git a/demos/supabase-todolist/lib/attachments/queue.dart b/demos/supabase-todolist/lib/attachments/queue.dart index 2a8dd9ca..70638716 100644 --- a/demos/supabase-todolist/lib/attachments/queue.dart +++ b/demos/supabase-todolist/lib/attachments/queue.dart @@ -1,90 +1,63 @@ import 'dart:async'; - +import 'dart:io'; +import 'dart:typed_data'; +import 'package:logging/logging.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:powersync/powersync.dart'; -import 'package:powersync_attachments_helper/powersync_attachments_helper.dart'; -import 'package:powersync_flutter_demo/app_config.dart'; +import 'package:powersync_attachments_stream/powersync_attachments_stream.dart'; import 'package:powersync_flutter_demo/attachments/remote_storage_adapter.dart'; -import 'package:powersync_flutter_demo/models/schema.dart'; - -/// Global reference to the queue -late final PhotoAttachmentQueue attachmentQueue; +late AttachmentQueue attachmentQueue; final remoteStorage = SupabaseStorageAdapter(); - -/// Function to handle errors when downloading attachments -/// Return false if you want to archive the attachment -Future onDownloadError(Attachment attachment, Object exception) async { - if (exception.toString().contains('Object not found')) { - return false; - } - return true; +final log = Logger('AttachmentQueue'); + +Future initializeAttachmentQueue(PowerSyncDatabase db) async { + // Use the app's document directory for local storage + final Directory appDocDir = await getApplicationDocumentsDirectory(); + final localStorage = IOLocalStorage(appDocDir); + + attachmentQueue = AttachmentQueue( + db: db, + remoteStorage: remoteStorage, + attachmentsDirectory: '${appDocDir.path}/attachments', + watchAttachments: () => db.watch(''' + SELECT photo_id as id FROM todos WHERE photo_id IS NOT NULL + ''').map((results) { + final items = results.map((row) => WatchedAttachmentItem(id: row['id'] as String, fileExtension: 'jpg')).toList(); + log.info('Watched attachment IDs: ${items.map((e) => e.id).toList()}'); + return items; + }), + localStorage: localStorage, + errorHandler: null, + ); + + await attachmentQueue.startSync(); } -class PhotoAttachmentQueue extends AbstractAttachmentQueue { - PhotoAttachmentQueue(db, remoteStorage) - : super( - db: db, - remoteStorage: remoteStorage, - onDownloadError: onDownloadError); - - @override - init() async { - if (AppConfig.supabaseStorageBucket.isEmpty) { - log.info( - 'No Supabase bucket configured, skip setting up PhotoAttachmentQueue watches'); - return; - } - - await super.init(); - } - - @override - Future saveFile(String fileId, int size, - {mediaType = 'image/jpeg'}) async { - String filename = '$fileId.jpg'; - - Attachment photoAttachment = Attachment( - id: fileId, - filename: filename, - state: AttachmentState.queuedUpload.index, - mediaType: mediaType, - localUri: getLocalFilePathSuffix(filename), - size: size, - ); - - return attachmentsService.saveAttachment(photoAttachment); - } - - @override - Future deleteFile(String fileId) async { - String filename = '$fileId.jpg'; - - Attachment photoAttachment = Attachment( - id: fileId, - filename: filename, - state: AttachmentState.queuedDelete.index); - - return attachmentsService.saveAttachment(photoAttachment); - } - - @override - StreamSubscription watchIds({String fileExtension = 'jpg'}) { - log.info('Watching photos in $todosTable...'); - return db.watch(''' - SELECT photo_id FROM $todosTable - WHERE photo_id IS NOT NULL - ''').map((results) { - return results.map((row) => row['photo_id'] as String).toList(); - }).listen((ids) async { - List idsInQueue = await attachmentsService.getAttachmentIds(); - List relevantIds = - ids.where((element) => !idsInQueue.contains(element)).toList(); - syncingService.processIds(relevantIds, fileExtension); - }); - } +Future savePhotoAttachment(Stream photoData, String todoId, + {String mediaType = 'image/jpeg'}) async { + + // Save the file using the AttachmentQueue API + return await attachmentQueue.saveFile( + data: photoData, + mediaType: mediaType, + fileExtension: 'jpg', + metaData: 'Photo attachment for todo: $todoId', + updateHook: (context, attachment) async { + // Update the todo item to reference this attachment + await context.execute( + 'UPDATE todos SET photo_id = ? WHERE id = ?', + [attachment.id, todoId], + ); + }, + ); } -initializeAttachmentQueue(PowerSyncDatabase db) async { - attachmentQueue = PhotoAttachmentQueue(db, remoteStorage); - await attachmentQueue.init(); +Future deletePhotoAttachment(String fileId) async { + return await attachmentQueue.deleteFile( + attachmentId: fileId, + updateHook: (context, attachment) async { + // Optionally update relationships in the same transaction + }, + ); } diff --git a/demos/supabase-todolist/lib/attachments/remote_storage_adapter.dart b/demos/supabase-todolist/lib/attachments/remote_storage_adapter.dart index 596c5da5..0a4da3b7 100644 --- a/demos/supabase-todolist/lib/attachments/remote_storage_adapter.dart +++ b/demos/supabase-todolist/lib/attachments/remote_storage_adapter.dart @@ -1,49 +1,95 @@ import 'dart:io'; import 'dart:typed_data'; -import 'package:powersync_attachments_helper/powersync_attachments_helper.dart'; +import 'package:powersync_attachments_stream/powersync_attachments_stream.dart'; import 'package:powersync_flutter_demo/app_config.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; -import 'package:image/image.dart' as img; +import 'package:logging/logging.dart'; class SupabaseStorageAdapter implements AbstractRemoteStorageAdapter { + static final _log = Logger('SupabaseStorageAdapter'); + @override - Future uploadFile(String filename, File file, - {String mediaType = 'text/plain'}) async { + Future uploadFile( + Stream> fileData, Attachment attachment) async { _checkSupabaseBucketIsConfigured(); + // Check if attachment size is specified (required for buffer allocation) + final byteSize = attachment.size; + if (byteSize == null) { + throw Exception('Cannot upload a file with no byte size specified'); + } + + _log.info('uploadFile: ${attachment.filename} (size: $byteSize bytes)'); + + // Collect all stream data into a single Uint8List buffer + final buffer = Uint8List(byteSize); + var position = 0; + + await for (final chunk in fileData) { + if (position + chunk.length > byteSize) { + throw Exception('File data exceeds specified size'); + } + buffer.setRange(position, position + chunk.length, chunk); + position += chunk.length; + } + + if (position != byteSize) { + throw Exception( + 'File data size ($position) does not match specified size ($byteSize)'); + } + + // Create a temporary file from the buffer for upload + final tempFile = + File('${Directory.systemTemp.path}/${attachment.filename}'); try { + await tempFile.writeAsBytes(buffer); + await Supabase.instance.client.storage .from(AppConfig.supabaseStorageBucket) - .upload(filename, file, - fileOptions: FileOptions(contentType: mediaType)); + .upload(attachment.filename, tempFile, + fileOptions: FileOptions( + contentType: + attachment.mediaType ?? 'application/octet-stream')); + + _log.info('Successfully uploaded ${attachment.filename}'); } catch (error) { + _log.severe('Error uploading ${attachment.filename}', error); throw Exception(error); + } finally { + if (await tempFile.exists()) { + await tempFile.delete(); + } } } @override - Future downloadFile(String filePath) async { + Future>> downloadFile(Attachment attachment) async { _checkSupabaseBucketIsConfigured(); try { + _log.info('downloadFile: ${attachment.filename}'); + Uint8List fileBlob = await Supabase.instance.client.storage .from(AppConfig.supabaseStorageBucket) - .download(filePath); - final image = img.decodeImage(fileBlob); - Uint8List blob = img.JpegEncoder().encode(image!); - return blob; + .download(attachment.filename); + + _log.info( + 'Successfully downloaded ${attachment.filename} (${fileBlob.length} bytes)'); + + // Return the raw file data as a stream + return Stream.value(fileBlob); } catch (error) { + _log.severe('Error downloading ${attachment.filename}', error); throw Exception(error); } } @override - Future deleteFile(String filename) async { + Future deleteFile(Attachment attachment) async { _checkSupabaseBucketIsConfigured(); - try { await Supabase.instance.client.storage .from(AppConfig.supabaseStorageBucket) - .remove([filename]); + .remove([attachment.filename]); } catch (error) { throw Exception(error); } diff --git a/demos/supabase-todolist/lib/models/schema.dart b/demos/supabase-todolist/lib/models/schema.dart index 89b69b0c..0ca9722d 100644 --- a/demos/supabase-todolist/lib/models/schema.dart +++ b/demos/supabase-todolist/lib/models/schema.dart @@ -1,5 +1,5 @@ import 'package:powersync/powersync.dart'; -import 'package:powersync_attachments_helper/powersync_attachments_helper.dart'; +import 'package:powersync_attachments_stream/powersync_attachments_stream.dart'; const todosTable = 'todos'; diff --git a/demos/supabase-todolist/lib/widgets/todo_item_widget.dart b/demos/supabase-todolist/lib/widgets/todo_item_widget.dart index a59812ed..c8d82a5f 100644 --- a/demos/supabase-todolist/lib/widgets/todo_item_widget.dart +++ b/demos/supabase-todolist/lib/widgets/todo_item_widget.dart @@ -23,7 +23,13 @@ class TodoItemWidget extends StatelessWidget { Future deleteTodo(TodoItem todo) async { if (todo.photoId != null) { - attachmentQueue.deleteFile(todo.photoId!); + + await attachmentQueue.deleteFile( + attachmentId: todo.photoId!, + updateHook: (context, attachment) async { + // await context.execute("UPDATE todos SET photo_id = NULL WHERE id = ?", [todo.id]); + }, + ); } await todo.delete(); } diff --git a/demos/supabase-todolist/pubspec.lock b/demos/supabase-todolist/pubspec.lock index a23cb023..0237ed2a 100644 --- a/demos/supabase-todolist/pubspec.lock +++ b/demos/supabase-todolist/pubspec.lock @@ -568,12 +568,19 @@ packages: source: path version: "1.15.0" powersync_attachments_helper: - dependency: "direct main" + dependency: "direct overridden" description: path: "../../packages/powersync_attachments_helper" relative: true source: path version: "0.6.18+11" + powersync_attachments_stream: + dependency: "direct main" + description: + path: "../../packages/powersync_attachments_stream" + relative: true + source: path + version: "0.0.1" powersync_core: dependency: "direct overridden" description: @@ -1050,5 +1057,5 @@ packages: source: hosted version: "2.1.0" sdks: - dart: ">=3.7.0 <4.0.0" + dart: ">=3.8.1 <4.0.0" flutter: ">=3.27.0" diff --git a/demos/supabase-todolist/pubspec.yaml b/demos/supabase-todolist/pubspec.yaml index f824de86..abd5987b 100644 --- a/demos/supabase-todolist/pubspec.yaml +++ b/demos/supabase-todolist/pubspec.yaml @@ -10,7 +10,8 @@ environment: dependencies: flutter: sdk: flutter - powersync_attachments_helper: ^0.6.18+11 + powersync_attachments_stream: + path: ../../packages/powersync_attachments_stream powersync: ^1.15.0 path_provider: ^2.1.1 supabase_flutter: ^2.0.1 diff --git a/packages/powersync_attachments_stream/README.md b/packages/powersync_attachments_stream/README.md index 4a260d8d..fdafabb4 100644 --- a/packages/powersync_attachments_stream/README.md +++ b/packages/powersync_attachments_stream/README.md @@ -1,39 +1,259 @@ - -TODO: Put a short description of the package here that helps potential users -know whether this package might be useful for them. +### Alpha Release -## Features +Attachment stream is currently in an alpha state, intended strictly for testing. Expect breaking changes and instability as development continues. -TODO: List what your package can do. Maybe include images, gifs, or videos. +Do not rely on this package for production use. -## Getting started +## Usage -TODO: List prerequisites and provide or point to information on how to -start using the package. +An `AttachmentQueue` is used to manage and sync attachments in your app. The attachments' state is stored in a local-only attachments table. -## Usage +### Key Assumptions + +- Each attachment is identified by a unique ID +- Attachments are immutable once created +- Relational data should reference attachments using a foreign key column +- Relational data should reflect the holistic state of attachments at any given time. An existing local attachment will be deleted locally if no relational data references it. + +### Example Implementation + +See the [Flutter Supabase Demo](../../demos/supabase-todolist/README.md) for a basic example of attachment syncing. + +In the example below, the user captures photos when checklist items are completed as part of an inspection workflow. + +1. First, define your schema including the `checklist` table and the local-only attachments table: + +```dart +final checklists = Table( + name: 'checklists', + columns: [ + Column.text('description'), + Column.integer('completed'), + Column.text('photo_id'), + ], +); + +final schema = Schema([ + UserRow.table, + // Add the local-only table which stores attachment states + // Learn more about this function below + createAttachmentsTable('attachments'), +]); +``` -TODO: Include short and useful examples for package users. Add longer examples -to `/example` folder. +2. Create an `AttachmentQueue` instance. This class provides default syncing utilities and implements a default sync strategy. This class is open and can be overridden for custom functionality: ```dart -const like = 'sample'; +final queue = AttachmentQueue( + db: db, + attachmentsDirectory: attachmentsDirectory, + remoteStorage: SupabaseRemoteStorage(supabase), + watchAttachments: () => db.watch( + ''' + SELECT photo_id + FROM checklists + WHERE photo_id IS NOT NULL + ''', + mapper: (row) => WatchedAttachmentItem( + id: row['photo_id'] as String, + fileExtension: 'jpg', + ), + ), +); ``` -## Additional information +* The `attachmentsDirectory` specifies where local attachment files should be stored. This directory needs to be provided to the constructor. In Flutter, `path_provider`'s `getApplicationDocumentsDirectory()` with a subdirectory like `/attachments` is a good choice. +* The `remoteStorage` is responsible for connecting to the attachments backend. See the `RemoteStorageAdapter` interface definition [here](https://github.com/powersync-ja/powersync-dart/blob/main/core/lib/src/attachments/remote_storage_adapter.dart). +* `watchAttachments` is a `Stream` of `WatchedAttachmentItem`. The `WatchedAttachmentItem`s represent the attachments which should be present in the application. We recommend using `PowerSync`'s `watch` query as shown above. In this example, we provide the `fileExtension` for all photos. This information could also be obtained from the query if necessary. + +3. Implement a `RemoteStorageAdapter` which interfaces with a remote storage provider. This will be used for downloading, uploading, and deleting attachments: + +```dart +final remote = _RemoteStorageAdapter(); + +class _RemoteStorageAdapter implements RemoteStorageAdapter { + @override + Stream> downloadFile(Attachment attachment) async* { + // TODO: Implement download from your backend + } + + @override + Future uploadFile(Stream> fileData, Attachment attachment) async { + // TODO: Implement upload to your backend + } + + @override + Future deleteFile(Attachment attachment) async { + // TODO: Implement delete in your backend + } +} +``` + +4. Start the sync process: + +```dart +await queue.startSync(); +``` + +5. Create and save attachments using `saveFile()`. This method will save the file to local storage, create an attachment record which queues the file for upload to the remote storage, and allows assigning the newly created attachment ID to a checklist item: + +```dart +await queue.saveFile( + data: Stream.value([0]), // Your attachment data + mediaType: 'image/jpg', + fileExtension: 'jpg', + onSave: (tx, attachment) async { + /** + * This callback is invoked in the same transaction which creates the attachment record. + * Assignments of the newly created photo_id should be done in the same transaction for maximum efficiency. + */ + await tx.execute( + ''' + UPDATE checklists + SET photo_id = ? + WHERE id = ? + ''', + [attachment.id, checklistId], + ); + }, +); +``` + +## Implementation Details + +### Attachment Table Structure + +The `createAttachmentsTable` function creates a local-only table for tracking attachment states. + +An attachments table definition can be created with the following options: + +| Option | Description | Default | +|--------|-----------------------|---------------| +| `name` | The name of the table | `attachments` | + +The default columns are: + +| Column Name | Type | Description | +|--------------|-----------|--------------------------------------------------------------------------------------------------------------------| +| `id` | `TEXT` | Unique identifier for the attachment | +| `filename` | `TEXT` | The filename of the attachment | +| `media_type` | `TEXT` | The media type of the attachment | +| `state` | `INTEGER` | Current state of the attachment (see `AttachmentState` enum) | +| `timestamp` | `INTEGER` | The timestamp of last update to the attachment | +| `size` | `INTEGER` | File size in bytes | +| `has_synced` | `INTEGER` | Internal flag tracking if the attachment has ever been synced (used for caching) | +| `meta_data` | `TEXT` | Additional metadata in JSON format | + +### Attachment States + +Attachments are managed through the following states: + +| State | Description | +|-------------------|-------------------------------------------------------------------------------| +| `QUEUED_UPLOAD` | Attachment is queued for upload to cloud storage | +| `QUEUED_DELETE` | Attachment is queued for deletion from cloud storage and local storage | +| `QUEUED_DOWNLOAD` | Attachment is queued for download from cloud storage | +| `SYNCED` | Attachment is fully synced | +| `ARCHIVED` | Attachment is orphaned - i.e., no longer referenced by any data | + +### Sync Process + +The `AttachmentQueue` implements a sync process with these components: + +1. **State Monitoring**: The queue watches the attachments table for records in `QUEUED_UPLOAD`, `QUEUED_DELETE`, and `QUEUED_DOWNLOAD` states. An event loop triggers calls to the remote storage for these operations. + +2. **Periodic Sync**: By default, the queue triggers a sync every 30 seconds to retry failed uploads/downloads, in particular after the app was offline. This interval can be configured by setting `syncInterval` in the `AttachmentQueue` constructor options, or disabled by setting the interval to `0`. + +3. **Watching State**: The `watchAttachments` stream in the `AttachmentQueue` constructor is used to maintain consistency between local and remote states: + - New items trigger downloads - see the Download Process below. + - Missing items trigger archiving - see Cache Management below. + +### Upload Process + +The `saveFile` method handles attachment creation and upload: + +1. The attachment is saved to local storage +2. An `AttachmentRecord` is created with `QUEUED_UPLOAD` state, linked to the local file using `localUri` +3. The attachment must be assigned to relational data in the same transaction, since this data is constantly watched and should always represent the attachment queue state +4. The `RemoteStorageAdapter` `uploadFile` function is called +5. On successful upload, the state changes to `SYNCED` +6. If upload fails, the record stays in `QUEUED_UPLOAD` state for retry + +### Download Process + +Attachments are scheduled for download when the stream from `watchAttachments` emits a new item that is not present locally: + +1. An `AttachmentRecord` is created with `QUEUED_DOWNLOAD` state +2. The `RemoteStorageAdapter` `downloadFile` function is called +3. The received data is saved to local storage +4. On successful download, the state changes to `SYNCED` +5. If download fails, the operation is retried in the next sync cycle + +### Delete Process + +The `deleteFile` method deletes attachments from both local and remote storage: + +1. The attachment record moves to `QUEUED_DELETE` state +2. The attachment must be unassigned from relational data in the same transaction, since this data is constantly watched and should always represent the attachment queue state +3. On successful deletion, the record is removed +4. If deletion fails, the operation is retried in the next sync cycle + +### Cache Management + +The `AttachmentQueue` implements a caching system for archived attachments: + +1. Local attachments are marked as `ARCHIVED` if the stream from `watchAttachments` no longer references them +2. Archived attachments are kept in the cache for potential future restoration +3. The cache size is controlled by the `archivedCacheLimit` parameter in the `AttachmentQueue` constructor +4. By default, the queue keeps the last 100 archived attachment records +5. When the cache limit is reached, the oldest archived attachments are permanently deleted +6. If an archived attachment is referenced again while still in the cache, it can be restored +7. The cache limit can be configured in the `AttachmentQueue` constructor + +### Error Handling + +1. **Automatic Retries**: + - Failed uploads/downloads/deletes are automatically retried + - The sync interval (default 30 seconds) ensures periodic retry attempts + - Retries continue indefinitely until successful + +2. **Custom Error Handling**: + - A `SyncErrorHandler` can be implemented to customize retry behavior (see example below) + - The handler can decide whether to retry or archive failed operations + - Different handlers can be provided for upload, download, and delete operations + +Example of a custom `SyncErrorHandler`: + +```dart +final errorHandler = _SyncErrorHandler(); + +class _SyncErrorHandler implements SyncErrorHandler { + @override + Future onDownloadError(Attachment attachment, Exception exception) async { + // TODO: Return if the attachment sync should be retried + return false; + } + + @override + Future onUploadError(Attachment attachment, Exception exception) async { + // TODO: Return if the attachment sync should be retried + return false; + } + + @override + Future onDeleteError(Attachment attachment, Exception exception) async { + // TODO: Return if the attachment sync should be retried + return false; + } +} -TODO: Tell users more about the package: where to find more information, how to -contribute to the package, how to file issues, what response they can expect -from the package authors, and more. +final queue = AttachmentQueue( + // ... other parameters ... + errorHandler: errorHandler, +); +``` \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/src/abstractions/attachment_context.dart b/packages/powersync_attachments_stream/lib/src/abstractions/attachment_context.dart index 34e76c25..268ed0b8 100644 --- a/packages/powersync_attachments_stream/lib/src/abstractions/attachment_context.dart +++ b/packages/powersync_attachments_stream/lib/src/abstractions/attachment_context.dart @@ -4,7 +4,7 @@ import '../attachment.dart'; /// /// This is typically provided through a locking/exclusivity method and allows /// safe, transactional operations on the attachment queue. -abstract class AttachmentContext { +abstract class AbstractAttachmentContext { /// Delete the attachment from the attachment queue. /// /// [id]: The ID of the attachment to delete. diff --git a/packages/powersync_attachments_stream/lib/src/abstractions/attachment_service.dart b/packages/powersync_attachments_stream/lib/src/abstractions/attachment_service.dart index 71e67c86..d8d26d9c 100644 --- a/packages/powersync_attachments_stream/lib/src/abstractions/attachment_service.dart +++ b/packages/powersync_attachments_stream/lib/src/abstractions/attachment_service.dart @@ -1,12 +1,12 @@ import 'attachment_context.dart'; /// Service for interacting with the local attachment records. -abstract class AttachmentService { +abstract class AbstractAttachmentService { /// Watcher for changes to attachments table. /// Once a change is detected it will initiate a sync of the attachments. Stream watchActiveAttachments(); /// Executes a callback with an exclusive lock on all attachment operations. /// This helps prevent race conditions between different updates. - Future withContext(Future Function(AttachmentContext context) action); + Future withContext(Future Function(AbstractAttachmentContext context) action); } \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/src/abstractions/local_storage.dart b/packages/powersync_attachments_stream/lib/src/abstractions/local_storage.dart index 2049143f..d69e39d3 100644 --- a/packages/powersync_attachments_stream/lib/src/abstractions/local_storage.dart +++ b/packages/powersync_attachments_stream/lib/src/abstractions/local_storage.dart @@ -1,6 +1,6 @@ import 'dart:typed_data'; -abstract class LocalStorage { +abstract class AbstractLocalStorageAdapter { /// Saves binary data stream to storage at the specified file path /// /// [filePath] - Path where the file will be stored diff --git a/packages/powersync_attachments_stream/lib/src/abstractions/remote_storage.dart b/packages/powersync_attachments_stream/lib/src/abstractions/remote_storage.dart index d954d7c4..80256a2f 100644 --- a/packages/powersync_attachments_stream/lib/src/abstractions/remote_storage.dart +++ b/packages/powersync_attachments_stream/lib/src/abstractions/remote_storage.dart @@ -2,7 +2,7 @@ import 'dart:async'; import '../attachment.dart'; /// Adapter for interfacing with remote attachment storage. -abstract class RemoteStorage { +abstract class AbstractRemoteStorageAdapter { /// Uploads a file to remote storage. /// /// [fileData] is a stream of byte arrays representing the file data. diff --git a/packages/powersync_attachments_stream/lib/src/attachment.dart b/packages/powersync_attachments_stream/lib/src/attachment.dart index 60b84a91..62e3eef4 100644 --- a/packages/powersync_attachments_stream/lib/src/attachment.dart +++ b/packages/powersync_attachments_stream/lib/src/attachment.dart @@ -4,6 +4,7 @@ import 'package:powersync_core/sqlite3_common.dart' show Row; import 'package:powersync_core/powersync_core.dart'; +import './attachment_queue_service.dart'; /// Represents the state of an attachment. enum AttachmentState { @@ -36,7 +37,7 @@ enum AttachmentState { int toInt() => index; } -const defaultAttachmentsQueueTableName = 'attachments_queue'; +const defaultAttachmentsQueueTableName = AttachmentQueue.defaultTableName; /// Represents an attachment with metadata and state information. /// diff --git a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart index ee69c81e..5f883948 100644 --- a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart +++ b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart @@ -70,10 +70,10 @@ class WatchedAttachmentItem { /// - [logger]: Logging interface used for all log operations. class AttachmentQueue { final PowerSyncDatabase db; - final RemoteStorage remoteStorage; + final AbstractRemoteStorageAdapter remoteStorage; final String attachmentsDirectory; final Stream> Function() watchAttachments; - final LocalStorage localStorage; + final AbstractLocalStorageAdapter localStorage; final String attachmentsQueueTableName; final SyncErrorHandler? errorHandler; final Duration syncInterval; @@ -87,7 +87,7 @@ class AttachmentQueue { final Mutex _mutex = Mutex(); bool _closed = false; StreamSubscription? _syncStatusSubscription; - late final AttachmentService attachmentsService; + late final AbstractAttachmentService attachmentsService; late final SyncingService syncingService; AttachmentQueue({ @@ -375,7 +375,7 @@ class AttachmentQueue { } /// Cleans up stale attachments. - Future _verifyAttachments(AttachmentContext context) async { + Future _verifyAttachments(AbstractAttachmentContext context) async { final attachments = await context.getActiveAttachments(); final List updates = []; diff --git a/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart b/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart index 6d71c38c..d097502f 100644 --- a/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart +++ b/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart @@ -4,7 +4,7 @@ import 'package:powersync_core/powersync_core.dart'; import 'package:powersync_core/sqlite3_common.dart'; import 'package:logging/logging.dart'; -class AttachmentContextImpl implements AttachmentContext { +class AttachmentContextImpl implements AbstractAttachmentContext { final PowerSyncDatabase db; final Logger log; final int maxArchivedCount; diff --git a/packages/powersync_attachments_stream/lib/src/implementations/attachment_service.dart b/packages/powersync_attachments_stream/lib/src/implementations/attachment_service.dart index 72332f0d..9d9ee4d3 100644 --- a/packages/powersync_attachments_stream/lib/src/implementations/attachment_service.dart +++ b/packages/powersync_attachments_stream/lib/src/implementations/attachment_service.dart @@ -7,14 +7,14 @@ import '../abstractions/attachment_context.dart'; import '../attachment.dart'; import 'attachment_context.dart'; -class AttachmentServiceImpl implements AttachmentService { +class AttachmentServiceImpl implements AbstractAttachmentService { final PowerSyncDatabase db; final Logger logger; final int maxArchivedCount; final String attachmentsQueueTableName; Future _mutex = Future.value(); - late final AttachmentContext _context; + late final AbstractAttachmentContext _context; AttachmentServiceImpl({ required this.db, @@ -54,7 +54,7 @@ class AttachmentServiceImpl implements AttachmentService { } @override - Future withContext(Future Function(AttachmentContext ctx) action) { + Future withContext(Future Function(AbstractAttachmentContext ctx) action) { // Simple mutex using chained futures final completer = Completer(); _mutex = _mutex.then((_) => action(_context)).then(completer.complete).catchError(completer.completeError); diff --git a/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart b/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart index f129e0aa..ee32adcd 100644 --- a/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart +++ b/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart @@ -19,10 +19,10 @@ import 'dart:typed_data'; import 'package:path/path.dart' as p; import '../abstractions/local_storage.dart'; -/// Implements [LocalStorage] for device filesystem using Dart IO. +/// Implements [AbstractLocalStorageAdapter] for device filesystem using Dart IO. /// /// Handles file and directory operations for attachments. -class IOLocalStorage implements LocalStorage { +class IOLocalStorage implements AbstractLocalStorageAdapter { final Directory baseDir; IOLocalStorage(this.baseDir); diff --git a/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart b/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart index 33d1ef53..4c1f77ac 100644 --- a/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart +++ b/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart @@ -32,9 +32,9 @@ import '../sync_error_handler.dart'; /// - [getLocalUri]: A function to resolve the local URI for a given filename. /// - [onDownloadError], [onUploadError], [onDeleteError]: Optional error handlers for managing sync-related errors. class SyncingService { - final RemoteStorage remoteStorage; - final LocalStorage localStorage; - final AttachmentService attachmentsService; + final AbstractRemoteStorageAdapter remoteStorage; + final AbstractLocalStorageAdapter localStorage; + final AbstractAttachmentService attachmentsService; final Future Function(String) getLocalUri; final SyncErrorHandler? errorHandler; final Duration syncThrottle; @@ -144,7 +144,7 @@ class SyncingService { /// [context]: The attachment context used for managing attachment states. Future handleSync( List attachments, - AttachmentContext context, + AbstractAttachmentContext context, ) async { logger.info( 'SyncingService: Starting handleSync with ${attachments.length} attachments', @@ -317,7 +317,7 @@ class SyncingService { /// /// [context]: The attachment context used to retrieve and manage archived attachments. /// Returns `true` if all archived attachments were successfully deleted, `false` otherwise. - Future deleteArchivedAttachments(AttachmentContext context) async { + Future deleteArchivedAttachments(AbstractAttachmentContext context) async { return context.deleteArchivedAttachments((pendingDelete) async { for (final attachment in pendingDelete) { if (attachment.localUri == null) continue; From af99d66c410d2c8ef526f7d45476ae38e2da3255 Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Mon, 11 Aug 2025 15:38:29 +0200 Subject: [PATCH 08/18] Refactor attachment queue initialization and implemented default base path for local storage in the powersync_attachments_stream package. Simplified the attachment watching logic. Updated README for clarity on schema and attachment states. --- .../lib/attachments/queue.dart | 18 +- .../powersync_attachments_stream/README.md | 169 +++++++++--------- .../lib/src/attachment_queue_service.dart | 12 +- .../lib/src/storage/io_local_storage.dart | 3 +- 4 files changed, 103 insertions(+), 99 deletions(-) diff --git a/demos/supabase-todolist/lib/attachments/queue.dart b/demos/supabase-todolist/lib/attachments/queue.dart index 70638716..54e1c672 100644 --- a/demos/supabase-todolist/lib/attachments/queue.dart +++ b/demos/supabase-todolist/lib/attachments/queue.dart @@ -14,7 +14,6 @@ final log = Logger('AttachmentQueue'); Future initializeAttachmentQueue(PowerSyncDatabase db) async { // Use the app's document directory for local storage final Directory appDocDir = await getApplicationDocumentsDirectory(); - final localStorage = IOLocalStorage(appDocDir); attachmentQueue = AttachmentQueue( db: db, @@ -22,21 +21,20 @@ Future initializeAttachmentQueue(PowerSyncDatabase db) async { attachmentsDirectory: '${appDocDir.path}/attachments', watchAttachments: () => db.watch(''' SELECT photo_id as id FROM todos WHERE photo_id IS NOT NULL - ''').map((results) { - final items = results.map((row) => WatchedAttachmentItem(id: row['id'] as String, fileExtension: 'jpg')).toList(); - log.info('Watched attachment IDs: ${items.map((e) => e.id).toList()}'); - return items; - }), - localStorage: localStorage, - errorHandler: null, + ''').map((results) => results + .map((row) => WatchedAttachmentItem( + id: row['id'] as String, + fileExtension: 'jpg', + )) + .toList()), ); await attachmentQueue.startSync(); } -Future savePhotoAttachment(Stream photoData, String todoId, +Future savePhotoAttachment( + Stream photoData, String todoId, {String mediaType = 'image/jpeg'}) async { - // Save the file using the AttachmentQueue API return await attachmentQueue.saveFile( data: photoData, diff --git a/packages/powersync_attachments_stream/README.md b/packages/powersync_attachments_stream/README.md index fdafabb4..9a10e9b3 100644 --- a/packages/powersync_attachments_stream/README.md +++ b/packages/powersync_attachments_stream/README.md @@ -29,46 +29,39 @@ In the example below, the user captures photos when checklist items are complete 1. First, define your schema including the `checklist` table and the local-only attachments table: ```dart -final checklists = Table( - name: 'checklists', - columns: [ +Schema schema = Schema(([ + const Table('checklists', [ Column.text('description'), Column.integer('completed'), Column.text('photo_id'), - ], -); - -final schema = Schema([ - UserRow.table, - // Add the local-only table which stores attachment states - // Learn more about this function below - createAttachmentsTable('attachments'), -]); + ]), + AttachmentsQueueTable( + attachmentsQueueTableName: defaultAttachmentsQueueTableName) +])); ``` 2. Create an `AttachmentQueue` instance. This class provides default syncing utilities and implements a default sync strategy. This class is open and can be overridden for custom functionality: ```dart +final Directory appDocDir = await getApplicationDocumentsDirectory(); + final queue = AttachmentQueue( - db: db, - attachmentsDirectory: attachmentsDirectory, - remoteStorage: SupabaseRemoteStorage(supabase), - watchAttachments: () => db.watch( - ''' - SELECT photo_id - FROM checklists - WHERE photo_id IS NOT NULL - ''', - mapper: (row) => WatchedAttachmentItem( - id: row['photo_id'] as String, - fileExtension: 'jpg', - ), - ), -); + db: db, + remoteStorage: remoteStorage, + attachmentsDirectory: '${appDocDir.path}/attachments', + watchAttachments: () => db.watch(''' + SELECT photo_id as id FROM todos WHERE photo_id IS NOT NULL + ''').map((results) => results + .map((row) => WatchedAttachmentItem( + id: row['id'] as String, + fileExtension: 'jpg', + )) + .toList()), + ); ``` * The `attachmentsDirectory` specifies where local attachment files should be stored. This directory needs to be provided to the constructor. In Flutter, `path_provider`'s `getApplicationDocumentsDirectory()` with a subdirectory like `/attachments` is a good choice. -* The `remoteStorage` is responsible for connecting to the attachments backend. See the `RemoteStorageAdapter` interface definition [here](https://github.com/powersync-ja/powersync-dart/blob/main/core/lib/src/attachments/remote_storage_adapter.dart). +* The `remoteStorage` is responsible for connecting to the attachments backend. See the `RemoteStorageAdapter` interface definition [here](https://github.com/powersync-ja/powersync.dart/blob/main/packages/powersync_attachments_stream/lib/src/abstractions/remote_storage.dart). * `watchAttachments` is a `Stream` of `WatchedAttachmentItem`. The `WatchedAttachmentItem`s represent the attachments which should be present in the application. We recommend using `PowerSync`'s `watch` query as shown above. In this example, we provide the `fileExtension` for all photos. This information could also be obtained from the query if necessary. 3. Implement a `RemoteStorageAdapter` which interfaces with a remote storage provider. This will be used for downloading, uploading, and deleting attachments: @@ -78,13 +71,13 @@ final remote = _RemoteStorageAdapter(); class _RemoteStorageAdapter implements RemoteStorageAdapter { @override - Stream> downloadFile(Attachment attachment) async* { - // TODO: Implement download from your backend + Future uploadFile(Stream> fileData, Attachment attachment) async { + // TODO: Implement upload to your backend } @override - Future uploadFile(Stream> fileData, Attachment attachment) async { - // TODO: Implement upload to your backend + Future>> downloadFile(Attachment attachment) async { + // TODO: Implement download from your backend } @override @@ -104,68 +97,76 @@ await queue.startSync(); ```dart await queue.saveFile( - data: Stream.value([0]), // Your attachment data - mediaType: 'image/jpg', - fileExtension: 'jpg', - onSave: (tx, attachment) async { - /** - * This callback is invoked in the same transaction which creates the attachment record. - * Assignments of the newly created photo_id should be done in the same transaction for maximum efficiency. - */ - await tx.execute( - ''' - UPDATE checklists - SET photo_id = ? - WHERE id = ? - ''', - [attachment.id, checklistId], - ); - }, -); + data: photoData, + mediaType: 'image/jpg', + fileExtension: 'jpg', + metaData: 'Test meta data', + updateHook: (context, attachment) async { + // Update the todo item to reference this attachment + await context.execute( + 'UPDATE checklists SET photo_id = ? WHERE id = ?', + [attachment.id, checklistId], + ); + }, + ); ``` ## Implementation Details ### Attachment Table Structure -The `createAttachmentsTable` function creates a local-only table for tracking attachment states. +The `AttachmentsQueueTable` class creates a **local-only table** for tracking the states and metadata of file attachments. It allows customization of the table name, additional columns, indexes, and optionally a view name. An attachments table definition can be created with the following options: -| Option | Description | Default | -|--------|-----------------------|---------------| -| `name` | The name of the table | `attachments` | +| Option | Description | Default | +| ---------------------- | -------------------------------| ----------------------------| +| `attachmentsQueueTableName` | The name of the table | `defaultAttachmentsQueueTableName` | +| `additionalColumns` | Extra columns to add to the table | `[]` (empty list) | +| `indexes` | Indexes to optimize queries | `[]` (empty list) | +| `viewName` | Optional associated view name | `null` | + +The default columns included in the table are: -The default columns are: +| Column Name | Type | Description | +| ------------ | --------- | -------------------------------------------------------------------------------- | +| `filename` | `TEXT` | The filename of the attachment | +| `local_uri` | `TEXT` | Local file URI or path | +| `timestamp` | `INTEGER` | The timestamp of the last update to the attachment | +| `size` | `INTEGER` | File size in bytes | +| `media_type` | `TEXT` | The media (MIME) type of the attachment | +| `state` | `INTEGER` | Current state of the attachment (e.g., queued, syncing, synced) | +| `has_synced` | `INTEGER` | Internal flag indicating if the attachment has ever been synced (for caching) | +| `meta_data` | `TEXT` | Additional metadata stored as JSON | -| Column Name | Type | Description | -|--------------|-----------|--------------------------------------------------------------------------------------------------------------------| -| `id` | `TEXT` | Unique identifier for the attachment | -| `filename` | `TEXT` | The filename of the attachment | -| `media_type` | `TEXT` | The media type of the attachment | -| `state` | `INTEGER` | Current state of the attachment (see `AttachmentState` enum) | -| `timestamp` | `INTEGER` | The timestamp of last update to the attachment | -| `size` | `INTEGER` | File size in bytes | -| `has_synced` | `INTEGER` | Internal flag tracking if the attachment has ever been synced (used for caching) | -| `meta_data` | `TEXT` | Additional metadata in JSON format | +The class extends a base `Table` class using a `localOnly` constructor, so this table exists **only locally** on the device and is not synchronized with a remote database. + +This design allows flexible tracking and management of attachment syncing state and metadata within the local database. | ### Attachment States -Attachments are managed through the following states: +Attachments are managed through the following states, which represent their current synchronization status with remote storage: + +| State | Description | +| ----------------- | ---------------------------------------------------------------------- | +| `queuedUpload` | Attachment is queued for upload to remote/cloud storage | +| `queuedDelete` | Attachment is queued for deletion from both remote and local storage | +| `queuedDownload` | Attachment is queued for download from remote/cloud storage | +| `synced` | Attachment is fully synchronized with remote storage | +| `archived` | Attachment is archived — no longer actively synchronized or referenced | + +--- + +The `AttachmentState` enum also provides helper methods for converting between the enum and its integer representation: -| State | Description | -|-------------------|-------------------------------------------------------------------------------| -| `QUEUED_UPLOAD` | Attachment is queued for upload to cloud storage | -| `QUEUED_DELETE` | Attachment is queued for deletion from cloud storage and local storage | -| `QUEUED_DOWNLOAD` | Attachment is queued for download from cloud storage | -| `SYNCED` | Attachment is fully synced | -| `ARCHIVED` | Attachment is orphaned - i.e., no longer referenced by any data | +- `AttachmentState.fromInt(int value)` — Constructs an `AttachmentState` from its corresponding integer index. Throws an `ArgumentError` if the value is out of range. +- `toInt()` — Returns the integer index of the current `AttachmentState` instance. ### Sync Process The `AttachmentQueue` implements a sync process with these components: -1. **State Monitoring**: The queue watches the attachments table for records in `QUEUED_UPLOAD`, `QUEUED_DELETE`, and `QUEUED_DOWNLOAD` states. An event loop triggers calls to the remote storage for these operations. +1. **State Monitoring**: The queue watches the attachments table for records in `queuedUpload`, `queuedDelete`, and `queuedDownload` states. An event loop triggers calls to the remote storage for these operations. 2. **Periodic Sync**: By default, the queue triggers a sync every 30 seconds to retry failed uploads/downloads, in particular after the app was offline. This interval can be configured by setting `syncInterval` in the `AttachmentQueue` constructor options, or disabled by setting the interval to `0`. @@ -178,27 +179,27 @@ The `AttachmentQueue` implements a sync process with these components: The `saveFile` method handles attachment creation and upload: 1. The attachment is saved to local storage -2. An `AttachmentRecord` is created with `QUEUED_UPLOAD` state, linked to the local file using `localUri` +2. An `AttachmentRecord` is created with `queuedUpload` state, linked to the local file using `localUri` 3. The attachment must be assigned to relational data in the same transaction, since this data is constantly watched and should always represent the attachment queue state 4. The `RemoteStorageAdapter` `uploadFile` function is called -5. On successful upload, the state changes to `SYNCED` -6. If upload fails, the record stays in `QUEUED_UPLOAD` state for retry +5. On successful upload, the state changes to `synced` +6. If upload fails, the record stays in `queuedUpload` state for retry ### Download Process Attachments are scheduled for download when the stream from `watchAttachments` emits a new item that is not present locally: -1. An `AttachmentRecord` is created with `QUEUED_DOWNLOAD` state +1. An `AttachmentRecord` is created with `queuedDownload` state 2. The `RemoteStorageAdapter` `downloadFile` function is called 3. The received data is saved to local storage -4. On successful download, the state changes to `SYNCED` +4. On successful download, the state changes to `synced` 5. If download fails, the operation is retried in the next sync cycle ### Delete Process The `deleteFile` method deletes attachments from both local and remote storage: -1. The attachment record moves to `QUEUED_DELETE` state +1. The attachment record moves to `queuedDelete` state 2. The attachment must be unassigned from relational data in the same transaction, since this data is constantly watched and should always represent the attachment queue state 3. On successful deletion, the record is removed 4. If deletion fails, the operation is retried in the next sync cycle @@ -207,7 +208,7 @@ The `deleteFile` method deletes attachments from both local and remote storage: The `AttachmentQueue` implements a caching system for archived attachments: -1. Local attachments are marked as `ARCHIVED` if the stream from `watchAttachments` no longer references them +1. Local attachments are marked as `archived` if the stream from `watchAttachments` no longer references them 2. Archived attachments are kept in the cache for potential future restoration 3. The cache size is controlled by the `archivedCacheLimit` parameter in the `AttachmentQueue` constructor 4. By default, the queue keeps the last 100 archived attachment records @@ -234,19 +235,19 @@ final errorHandler = _SyncErrorHandler(); class _SyncErrorHandler implements SyncErrorHandler { @override - Future onDownloadError(Attachment attachment, Exception exception) async { + Future onDownloadError(Attachment attachment, Object exception) async { // TODO: Return if the attachment sync should be retried return false; } @override - Future onUploadError(Attachment attachment, Exception exception) async { + Future onUploadError(Attachment attachment, Object exception) async { // TODO: Return if the attachment sync should be retried return false; } @override - Future onDeleteError(Attachment attachment, Exception exception) async { + Future onDeleteError(Attachment attachment, Object exception) async { // TODO: Return if the attachment sync should be retried return false; } diff --git a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart index 5f883948..f0673c26 100644 --- a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart +++ b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:logging/logging.dart'; +import './storage/io_local_storage.dart'; import 'package:powersync_core/powersync_core.dart'; import 'package:sqlite_async/mutex.dart'; import 'attachment.dart'; @@ -29,10 +30,13 @@ import 'sync/syncing_service.dart'; class WatchedAttachmentItem { /// Id for the attachment record. final String id; + /// File extension used to determine an internal filename for storage if no [filename] is provided. final String? fileExtension; + /// Filename to store the attachment with. final String? filename; + /// Optional metadata for the attachment record. final String? metaData; @@ -95,7 +99,7 @@ class AttachmentQueue { required this.remoteStorage, required this.attachmentsDirectory, required this.watchAttachments, - required this.localStorage, + IOLocalStorage? localStorage, this.attachmentsQueueTableName = defaultTableName, this.errorHandler, this.syncInterval = const Duration(seconds: 30), @@ -103,7 +107,8 @@ class AttachmentQueue { this.syncThrottleDuration = const Duration(seconds: 1), this.downloadAttachments = true, Logger? logger, - }) : logger = logger ?? Logger('AttachmentQueue') { + }) : localStorage = localStorage ?? IOLocalStorage(), + logger = logger ?? Logger('AttachmentQueue') { attachmentsService = AttachmentServiceImpl( db: db, logger: logger ?? Logger('AttachmentQueue'), @@ -113,7 +118,7 @@ class AttachmentQueue { syncingService = SyncingService( remoteStorage: remoteStorage, - localStorage: localStorage, + localStorage: this.localStorage, attachmentsService: attachmentsService, getLocalUri: (filename) async => getLocalUri(filename), errorHandler: errorHandler, @@ -229,7 +234,6 @@ class AttachmentQueue { // hasSynced: existingQueueItem.hasSynced; ), ); - } else if (existingQueueItem.state == AttachmentState.archived) { // The attachment is present again. Need to queue it for sync. if (existingQueueItem.hasSynced) { diff --git a/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart b/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart index ee32adcd..bca1324c 100644 --- a/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart +++ b/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart @@ -25,7 +25,8 @@ import '../abstractions/local_storage.dart'; class IOLocalStorage implements AbstractLocalStorageAdapter { final Directory baseDir; - IOLocalStorage(this.baseDir); + IOLocalStorage([Directory? baseDir]) + : baseDir = baseDir ?? Directory.systemTemp; File _fileFor(String filePath) => File(p.join(baseDir.path, filePath)); File _metaFileFor(String filePath) => From 162552df1e03436b67cb4c8bf03f26a283075c26 Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Mon, 11 Aug 2025 15:48:06 +0200 Subject: [PATCH 09/18] Removed redundant logger names. --- .../lib/attachments/queue.dart | 1 + .../lib/src/sync/syncing_service.dart | 43 ++++++++----------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/demos/supabase-todolist/lib/attachments/queue.dart b/demos/supabase-todolist/lib/attachments/queue.dart index 54e1c672..b1a2dc8f 100644 --- a/demos/supabase-todolist/lib/attachments/queue.dart +++ b/demos/supabase-todolist/lib/attachments/queue.dart @@ -18,6 +18,7 @@ Future initializeAttachmentQueue(PowerSyncDatabase db) async { attachmentQueue = AttachmentQueue( db: db, remoteStorage: remoteStorage, + logger: log, attachmentsDirectory: '${appDocDir.path}/attachments', watchAttachments: () => db.watch(''' SELECT photo_id as id FROM todos WHERE photo_id IS NOT NULL diff --git a/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart b/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart index 4c1f77ac..78f80e0a 100644 --- a/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart +++ b/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart @@ -78,10 +78,7 @@ class SyncingService { await attachmentsService.withContext((context) async { final attachments = await context.getActiveAttachments(); logger.info( - 'active attachments: ${attachments.map((e) => e.id).toList()}', - ); - logger.info( - 'SyncingService: Found ${attachments.length} active attachments', + 'Found ${attachments.length} active attachments', ); await handleSync(attachments, context); await deleteArchivedAttachments(context); @@ -147,42 +144,42 @@ class SyncingService { AbstractAttachmentContext context, ) async { logger.info( - 'SyncingService: Starting handleSync with ${attachments.length} attachments', + 'Starting handleSync with ${attachments.length} attachments', ); final updatedAttachments = []; for (final attachment in attachments) { logger.info( - 'SyncingService: Processing attachment ${attachment.id} with state: ${attachment.state}', + 'Processing attachment ${attachment.id} with state: ${attachment.state}', ); try { switch (attachment.state) { case AttachmentState.queuedDownload: - logger.info('SyncingService: Downloading [${attachment.filename}]'); + logger.info('Downloading [${attachment.filename}]'); updatedAttachments.add(await downloadAttachment(attachment)); break; case AttachmentState.queuedUpload: - logger.info('SyncingService: Uploading [${attachment.filename}]'); + logger.info('Uploading [${attachment.filename}]'); updatedAttachments.add(await uploadAttachment(attachment)); break; case AttachmentState.queuedDelete: - logger.info('SyncingService: Deleting [${attachment.filename}]'); + logger.info('Deleting [${attachment.filename}]'); updatedAttachments.add(await deleteAttachment(attachment)); break; case AttachmentState.synced: logger.info( - 'SyncingService: Attachment ${attachment.id} is already synced', + 'Attachment ${attachment.id} is already synced', ); break; case AttachmentState.archived: logger.info( - 'SyncingService: Attachment ${attachment.id} is archived', + 'Attachment ${attachment.id} is archived', ); break; } } catch (e, st) { logger.warning( - 'SyncingService: Error during sync for ${attachment.id}', + 'Error during sync for ${attachment.id}', e, st, ); @@ -191,7 +188,7 @@ class SyncingService { if (updatedAttachments.isNotEmpty) { logger.info( - 'SyncingService: Saving ${updatedAttachments.length} updated attachments', + 'Saving ${updatedAttachments.length} updated attachments', ); await context.saveAttachments(updatedAttachments); } @@ -203,7 +200,7 @@ class SyncingService { /// Returns the updated attachment with its new state. Future uploadAttachment(Attachment attachment) async { logger.info( - 'SyncingService: Starting upload for attachment ${attachment.id}', + 'Starting upload for attachment ${attachment.id}', ); try { if (attachment.localUri == null) { @@ -214,7 +211,7 @@ class SyncingService { attachment, ); logger.info( - 'SyncingService: Successfully uploaded attachment "${attachment.id}" to Cloud Storage', + 'Successfully uploaded attachment "${attachment.id}" to Cloud Storage', ); return attachment.copyWith( state: AttachmentState.synced, @@ -222,7 +219,7 @@ class SyncingService { ); } catch (e, st) { logger.warning( - 'SyncingService: Upload attachment error for attachment $attachment', + 'Upload attachment error for attachment $attachment', e, st, ); @@ -230,7 +227,7 @@ class SyncingService { final shouldRetry = await errorHandler!.onUploadError(attachment, e); if (!shouldRetry) { logger.info( - 'SyncingService: Attachment with ID ${attachment.id} has been archived', + 'Attachment with ID ${attachment.id} has been archived', ); return attachment.copyWith(state: AttachmentState.archived); } @@ -245,7 +242,7 @@ class SyncingService { /// Returns the updated attachment with its new state. Future downloadAttachment(Attachment attachment) async { logger.info( - 'SyncingService: Starting download for attachment ${attachment.id}', + 'Starting download for attachment ${attachment.id}', ); final attachmentPath = await getLocalUri(attachment.filename); try { @@ -255,11 +252,9 @@ class SyncingService { fileStream.map((chunk) => Uint8List.fromList(chunk)), ); logger.info( - 'SyncingService: Successfully downloaded file "${attachment.id}"', + 'Successfully downloaded file "${attachment.id}"', ); - logger.info('downloadAttachmentXY $attachment'); - return attachment.copyWith( localUri: attachmentPath, state: AttachmentState.synced, @@ -270,13 +265,13 @@ class SyncingService { final shouldRetry = await errorHandler!.onDownloadError(attachment, e); if (!shouldRetry) { logger.info( - 'SyncingService: Attachment with ID ${attachment.id} has been archived', + 'Attachment with ID ${attachment.id} has been archived', ); return attachment.copyWith(state: AttachmentState.archived); } } logger.warning( - 'SyncingService: Download attachment error for attachment $attachment', + 'Download attachment error for attachment $attachment', e, st, ); @@ -291,7 +286,7 @@ class SyncingService { Future deleteAttachment(Attachment attachment) async { try { logger.info( - 'SyncingService: Deleting attachment ${attachment.id} from remote storage', + 'Deleting attachment ${attachment.id} from remote storage', ); await remoteStorage.deleteFile(attachment); From 1f696a07fba5d891ebde6dccbfb4c77be402b4e5 Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Mon, 11 Aug 2025 16:04:35 +0200 Subject: [PATCH 10/18] Renamed SyncErrorHandler to AbstractSyncErrorHandler. Updated related classes to utilize the new interface and adjusted README for consistency. --- packages/powersync_attachments_stream/README.md | 2 +- packages/powersync_attachments_stream/lib/common.dart | 2 +- .../lib/src/{ => abstractions}/sync_error_handler.dart | 4 ++-- .../lib/src/attachment_queue_service.dart | 5 ++--- .../lib/src/sync/syncing_service.dart | 4 ++-- 5 files changed, 8 insertions(+), 9 deletions(-) rename packages/powersync_attachments_stream/lib/src/{ => abstractions}/sync_error_handler.dart (95%) diff --git a/packages/powersync_attachments_stream/README.md b/packages/powersync_attachments_stream/README.md index 9a10e9b3..851ac2d6 100644 --- a/packages/powersync_attachments_stream/README.md +++ b/packages/powersync_attachments_stream/README.md @@ -233,7 +233,7 @@ Example of a custom `SyncErrorHandler`: ```dart final errorHandler = _SyncErrorHandler(); -class _SyncErrorHandler implements SyncErrorHandler { +class _SyncErrorHandler implements AbstractSyncErrorHandler { @override Future onDownloadError(Attachment attachment, Object exception) async { // TODO: Return if the attachment sync should be retried diff --git a/packages/powersync_attachments_stream/lib/common.dart b/packages/powersync_attachments_stream/lib/common.dart index 14e01eb8..9c8bd8bd 100644 --- a/packages/powersync_attachments_stream/lib/common.dart +++ b/packages/powersync_attachments_stream/lib/common.dart @@ -1,6 +1,6 @@ /// Platform-agnostic exports for all platforms (including web). export 'src/attachment.dart'; -export 'src/sync_error_handler.dart'; +export 'src/abstractions/sync_error_handler.dart'; export 'src/abstractions/attachment_service.dart'; export 'src/abstractions/attachment_context.dart'; export 'src/abstractions/local_storage.dart'; diff --git a/packages/powersync_attachments_stream/lib/src/sync_error_handler.dart b/packages/powersync_attachments_stream/lib/src/abstractions/sync_error_handler.dart similarity index 95% rename from packages/powersync_attachments_stream/lib/src/sync_error_handler.dart rename to packages/powersync_attachments_stream/lib/src/abstractions/sync_error_handler.dart index c6a5795b..cc2e3cab 100644 --- a/packages/powersync_attachments_stream/lib/src/sync_error_handler.dart +++ b/packages/powersync_attachments_stream/lib/src/abstractions/sync_error_handler.dart @@ -1,9 +1,9 @@ -import './attachment.dart'; +import '../attachment.dart'; /// Interface for handling errors during attachment operations. /// Implementations determine whether failed operations should be retried. /// Attachment records are archived if an operation fails and should not be retried. -abstract class SyncErrorHandler { +abstract class AbstractSyncErrorHandler { /// Determines whether the provided attachment download operation should be retried. /// /// [attachment] The attachment involved in the failed download operation. diff --git a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart index f0673c26..5a8d4b5d 100644 --- a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart +++ b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart @@ -15,7 +15,7 @@ import 'abstractions/attachment_service.dart'; import 'abstractions/attachment_context.dart'; import 'abstractions/local_storage.dart'; import 'abstractions/remote_storage.dart'; -import 'sync_error_handler.dart'; +import 'abstractions/sync_error_handler.dart'; import 'implementations/attachment_service.dart'; import 'sync/syncing_service.dart'; @@ -79,7 +79,7 @@ class AttachmentQueue { final Stream> Function() watchAttachments; final AbstractLocalStorageAdapter localStorage; final String attachmentsQueueTableName; - final SyncErrorHandler? errorHandler; + final AbstractSyncErrorHandler? errorHandler; final Duration syncInterval; final int archivedCacheLimit; final Duration syncThrottleDuration; @@ -125,7 +125,6 @@ class AttachmentQueue { syncThrottle: syncThrottleDuration, period: syncInterval, ); - logger?.info('syncingService: $syncingService'); } /// Initialize the attachment queue by: diff --git a/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart b/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart index 78f80e0a..8b9687c7 100644 --- a/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart +++ b/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart @@ -17,7 +17,7 @@ import '../abstractions/attachment_context.dart'; import '../attachment.dart'; import '../abstractions/local_storage.dart'; import '../abstractions/remote_storage.dart'; -import '../sync_error_handler.dart'; +import '../abstractions/sync_error_handler.dart'; /// SyncingService is responsible for syncing attachments between local and remote storage. /// @@ -36,7 +36,7 @@ class SyncingService { final AbstractLocalStorageAdapter localStorage; final AbstractAttachmentService attachmentsService; final Future Function(String) getLocalUri; - final SyncErrorHandler? errorHandler; + final AbstractSyncErrorHandler? errorHandler; final Duration syncThrottle; final Duration period; final Logger logger; From fd0175d799d93db8358b7ef2708e19e4dfc84d88 Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Tue, 12 Aug 2025 14:33:18 +0200 Subject: [PATCH 11/18] Refactor local storage implementation in powersync_attachments_stream package. Updated attachment queue initialization to use localStorage, and enhanced README for clarity on storage handling. Added tests for edge cases and robustness in local storage operations. --- .../lib/attachments/photo_widget.dart | 6 +- .../lib/attachments/queue.dart | 3 +- .../powersync_attachments_stream/README.md | 7 +- .../analysis_options.yaml | 6 + .../lib/src/abstractions/local_storage.dart | 18 +- .../lib/src/attachment_queue_service.dart | 29 +-- .../implementations/attachment_context.dart | 5 +- .../implementations/attachment_service.dart | 30 ++- .../lib/src/storage/io_local_storage.dart | 45 +--- .../lib/src/sync/syncing_service.dart | 18 +- .../test/local_storage_test.dart | 220 ++++++++++-------- 11 files changed, 191 insertions(+), 196 deletions(-) diff --git a/demos/supabase-todolist/lib/attachments/photo_widget.dart b/demos/supabase-todolist/lib/attachments/photo_widget.dart index 9de3303a..ecf5a94e 100644 --- a/demos/supabase-todolist/lib/attachments/photo_widget.dart +++ b/demos/supabase-todolist/lib/attachments/photo_widget.dart @@ -1,5 +1,6 @@ import 'dart:io'; - +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as p; import 'package:flutter/material.dart'; import 'package:powersync_attachments_stream/powersync_attachments_stream.dart'; import 'package:powersync_flutter_demo/attachments/camera_helpers.dart'; @@ -37,7 +38,8 @@ class _PhotoWidgetState extends State { if (photoId == null) { return _ResolvedPhotoState(photoPath: null, fileExists: false); } - photoPath = await attachmentQueue.getLocalUri('$photoId.jpg'); + final appDocDir = await getApplicationDocumentsDirectory(); + photoPath = p.join(appDocDir.path, '$photoId.jpg'); bool fileExists = await File(photoPath).exists(); diff --git a/demos/supabase-todolist/lib/attachments/queue.dart b/demos/supabase-todolist/lib/attachments/queue.dart index b1a2dc8f..7dcd7b29 100644 --- a/demos/supabase-todolist/lib/attachments/queue.dart +++ b/demos/supabase-todolist/lib/attachments/queue.dart @@ -19,7 +19,8 @@ Future initializeAttachmentQueue(PowerSyncDatabase db) async { db: db, remoteStorage: remoteStorage, logger: log, - attachmentsDirectory: '${appDocDir.path}/attachments', + localStorage: IOLocalStorage(appDocDir.path), + // attachmentsDirectory: '${appDocDir.path}/attachments', watchAttachments: () => db.watch(''' SELECT photo_id as id FROM todos WHERE photo_id IS NOT NULL ''').map((results) => results diff --git a/packages/powersync_attachments_stream/README.md b/packages/powersync_attachments_stream/README.md index 851ac2d6..f706ba5d 100644 --- a/packages/powersync_attachments_stream/README.md +++ b/packages/powersync_attachments_stream/README.md @@ -44,11 +44,12 @@ Schema schema = Schema(([ ```dart final Directory appDocDir = await getApplicationDocumentsDirectory(); +final localStorage = IOLocalStorage('${appDocDir.path}/attachments'); final queue = AttachmentQueue( db: db, remoteStorage: remoteStorage, - attachmentsDirectory: '${appDocDir.path}/attachments', + localStorage: localStorage, watchAttachments: () => db.watch(''' SELECT photo_id as id FROM todos WHERE photo_id IS NOT NULL ''').map((results) => results @@ -60,7 +61,7 @@ final queue = AttachmentQueue( ); ``` -* The `attachmentsDirectory` specifies where local attachment files should be stored. This directory needs to be provided to the constructor. In Flutter, `path_provider`'s `getApplicationDocumentsDirectory()` with a subdirectory like `/attachments` is a good choice. +* The `localStorage` is an implementation of `AbstractLocalStorageAdapter` that specifies where and how local attachment files should be stored. For mobile and desktop apps, `IOLocalStorage` can be used, which requires a directory path. In Flutter, `path_provider`'s `getApplicationDocumentsDirectory()` with a subdirectory like `/attachments` is a good choice. * The `remoteStorage` is responsible for connecting to the attachments backend. See the `RemoteStorageAdapter` interface definition [here](https://github.com/powersync-ja/powersync.dart/blob/main/packages/powersync_attachments_stream/lib/src/abstractions/remote_storage.dart). * `watchAttachments` is a `Stream` of `WatchedAttachmentItem`. The `WatchedAttachmentItem`s represent the attachments which should be present in the application. We recommend using `PowerSync`'s `watch` query as shown above. In this example, we provide the `fileExtension` for all photos. This information could also be obtained from the query if necessary. @@ -69,7 +70,7 @@ final queue = AttachmentQueue( ```dart final remote = _RemoteStorageAdapter(); -class _RemoteStorageAdapter implements RemoteStorageAdapter { +class _RemoteStorageAdapter implements AbstractRemoteStorageAdapter { @override Future uploadFile(Stream> fileData, Attachment attachment) async { // TODO: Implement upload to your backend diff --git a/packages/powersync_attachments_stream/analysis_options.yaml b/packages/powersync_attachments_stream/analysis_options.yaml index a5744c1c..79238a7a 100644 --- a/packages/powersync_attachments_stream/analysis_options.yaml +++ b/packages/powersync_attachments_stream/analysis_options.yaml @@ -1,4 +1,10 @@ include: package:flutter_lints/flutter.yaml +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/packages/powersync_attachments_stream/lib/src/abstractions/local_storage.dart b/packages/powersync_attachments_stream/lib/src/abstractions/local_storage.dart index d69e39d3..ddc85b8b 100644 --- a/packages/powersync_attachments_stream/lib/src/abstractions/local_storage.dart +++ b/packages/powersync_attachments_stream/lib/src/abstractions/local_storage.dart @@ -26,19 +26,9 @@ abstract class AbstractLocalStorageAdapter { /// Returns true if the file exists, false otherwise Future fileExists(String filePath); - /// Creates a directory at the specified path - /// - /// [path] - Path of the directory to create - Future makeDir(String path); - - /// Recursively removes a directory and its contents - /// - /// [path] - Path of the directory to remove - Future rmDir(String path); + /// Initializes the storage, performing any necessary setup. + Future initialize(); - /// Copies a file from source to target path - /// - /// [sourcePath] - Path of the source file - /// [targetPath] - Path where the file will be copied - Future copyFile(String sourcePath, String targetPath); + /// Clears all data from the storage. + Future clear(); } diff --git a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart index 5a8d4b5d..bc81db0f 100644 --- a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart +++ b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart @@ -7,7 +7,6 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:logging/logging.dart'; -import './storage/io_local_storage.dart'; import 'package:powersync_core/powersync_core.dart'; import 'package:sqlite_async/mutex.dart'; import 'attachment.dart'; @@ -75,7 +74,6 @@ class WatchedAttachmentItem { class AttachmentQueue { final PowerSyncDatabase db; final AbstractRemoteStorageAdapter remoteStorage; - final String attachmentsDirectory; final Stream> Function() watchAttachments; final AbstractLocalStorageAdapter localStorage; final String attachmentsQueueTableName; @@ -90,16 +88,15 @@ class AttachmentQueue { final Mutex _mutex = Mutex(); bool _closed = false; - StreamSubscription? _syncStatusSubscription; + StreamSubscription>? _syncStatusSubscription; late final AbstractAttachmentService attachmentsService; late final SyncingService syncingService; AttachmentQueue({ required this.db, required this.remoteStorage, - required this.attachmentsDirectory, required this.watchAttachments, - IOLocalStorage? localStorage, + required this.localStorage, this.attachmentsQueueTableName = defaultTableName, this.errorHandler, this.syncInterval = const Duration(seconds: 30), @@ -107,8 +104,7 @@ class AttachmentQueue { this.syncThrottleDuration = const Duration(seconds: 1), this.downloadAttachments = true, Logger? logger, - }) : localStorage = localStorage ?? IOLocalStorage(), - logger = logger ?? Logger('AttachmentQueue') { + }) : logger = logger ?? Logger('AttachmentQueue') { attachmentsService = AttachmentServiceImpl( db: db, logger: logger ?? Logger('AttachmentQueue'), @@ -118,9 +114,8 @@ class AttachmentQueue { syncingService = SyncingService( remoteStorage: remoteStorage, - localStorage: this.localStorage, + localStorage: localStorage, attachmentsService: attachmentsService, - getLocalUri: (filename) async => getLocalUri(filename), errorHandler: errorHandler, syncThrottle: syncThrottleDuration, period: syncInterval, @@ -139,7 +134,7 @@ class AttachmentQueue { await _stopSyncingInternal(); - await localStorage.makeDir(attachmentsDirectory); + await localStorage.initialize(); await attachmentsService.withContext((context) async { await _verifyAttachments(context); @@ -256,7 +251,7 @@ class AttachmentQueue { } // Archive any items not specified in the watched items. - // For QUEUED_DELETE or QUEUED_UPLOAD states, archive only if hasSynced is true. + // For queuedDelete or queuedUpload states, archive only if hasSynced is true. // For other states, archive if the record is not found in the items. for (final attachment in currentAttachments) { final notInWatchedItems = items.every( @@ -301,10 +296,9 @@ class AttachmentQueue { id, fileExtension, ); - final String localUri = getLocalUri(filename); // Write the file to the filesystem. - final fileSize = await localStorage.saveFile(localUri, data); + final fileSize = await localStorage.saveFile(filename, data); return await attachmentsService.withContext((attachmentContext) async { return await db.writeTransaction((tx) async { @@ -314,7 +308,7 @@ class AttachmentQueue { size: fileSize, mediaType: mediaType, state: AttachmentState.queuedUpload, - localUri: localUri, + localUri: filename, metaData: metaData, ); @@ -354,11 +348,6 @@ class AttachmentQueue { }); } - /// Returns the user's storage directory with the attachment path used to load the file. - String getLocalUri(String filename) { - return '$attachmentsDirectory/$filename'; - } - /// Removes all archived items. Future expireCache() async { await attachmentsService.withContext((context) async { @@ -374,7 +363,7 @@ class AttachmentQueue { await attachmentsService.withContext((context) async { await context.clearQueue(); }); - await localStorage.rmDir(attachmentsDirectory); + await localStorage.clear(); } /// Cleans up stale attachments. diff --git a/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart b/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart index d097502f..8c7176af 100644 --- a/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart +++ b/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart @@ -124,8 +124,9 @@ class AttachmentContextImpl implements AbstractAttachmentContext { // Delete the archived attachments from the table final ids = archivedAttachments.map((a) => a.id).toList(); if (ids.isNotEmpty) { - final placeholders = List.filled(ids.length, '?').join(','); - await db.execute('DELETE FROM $table WHERE id IN ($placeholders)', ids); + await db.executeBatch('DELETE FROM $table WHERE id = ?', [ + for (final id in ids) [id], + ]); } log.info('Deleted ${archivedAttachments.length} archived attachments.'); diff --git a/packages/powersync_attachments_stream/lib/src/implementations/attachment_service.dart b/packages/powersync_attachments_stream/lib/src/implementations/attachment_service.dart index 9d9ee4d3..9cb263aa 100644 --- a/packages/powersync_attachments_stream/lib/src/implementations/attachment_service.dart +++ b/packages/powersync_attachments_stream/lib/src/implementations/attachment_service.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:logging/logging.dart'; import 'package:powersync_core/powersync_core.dart'; +import 'package:sqlite_async/sqlite_async.dart'; import '../abstractions/attachment_service.dart'; import '../abstractions/attachment_context.dart'; @@ -12,7 +13,7 @@ class AttachmentServiceImpl implements AbstractAttachmentService { final Logger logger; final int maxArchivedCount; final String attachmentsQueueTableName; - Future _mutex = Future.value(); + final Mutex _mutex = Mutex(); late final AbstractAttachmentContext _context; @@ -22,13 +23,18 @@ class AttachmentServiceImpl implements AbstractAttachmentService { required this.maxArchivedCount, required this.attachmentsQueueTableName, }) { - _context = AttachmentContextImpl(db, logger, maxArchivedCount, attachmentsQueueTableName); + _context = AttachmentContextImpl( + db, + logger, + maxArchivedCount, + attachmentsQueueTableName, + ); } @override Stream watchActiveAttachments() async* { logger.info('Watching attachments...'); - + // Watch for attachments with active states (queued for upload, download, or delete) final stream = db.watch( ''' @@ -54,10 +60,16 @@ class AttachmentServiceImpl implements AbstractAttachmentService { } @override - Future withContext(Future Function(AbstractAttachmentContext ctx) action) { - // Simple mutex using chained futures - final completer = Completer(); - _mutex = _mutex.then((_) => action(_context)).then(completer.complete).catchError(completer.completeError); - return completer.future; + Future withContext( + Future Function(AbstractAttachmentContext ctx) action, + ) async { + return await _mutex.lock(() async { + try { + return await action(_context); + } catch (e, stackTrace) { + // Re-throw the error to be handled by the caller + Error.throwWithStackTrace(e, stackTrace); + } + }); } -} \ No newline at end of file +} diff --git a/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart b/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart index bca1324c..9a118178 100644 --- a/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart +++ b/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart @@ -23,10 +23,12 @@ import '../abstractions/local_storage.dart'; /// /// Handles file and directory operations for attachments. class IOLocalStorage implements AbstractLocalStorageAdapter { - final Directory baseDir; + final String attachmentsDirectory; + late final Directory baseDir; - IOLocalStorage([Directory? baseDir]) - : baseDir = baseDir ?? Directory.systemTemp; + IOLocalStorage(this.attachmentsDirectory) { + baseDir = Directory(attachmentsDirectory); + } File _fileFor(String filePath) => File(p.join(baseDir.path, filePath)); File _metaFileFor(String filePath) => @@ -88,40 +90,15 @@ class IOLocalStorage implements AbstractLocalStorageAdapter { /// Creates a directory and all necessary parent directories dynamically if they do not exist. @override - Future makeDir(String path) async { - await Directory(p.join(baseDir.path, path)).create(recursive: true); + Future initialize() async { + await baseDir.create(recursive: true); } - /// Recursively removes a directory and all its contents. @override - Future rmDir(String path) async { - final dir = Directory(p.join(baseDir.path, path)); - if (await dir.exists()) { - await for (final entity in dir.list(recursive: false)) { - if (entity is Directory) { - await rmDir(p.relative(entity.path, from: baseDir.path)); - } else if (entity is File) { - await entity.delete(); - } - } - await dir.delete(); - } - } - - /// Copies a file and its metadata to a new location. - @override - Future copyFile(String sourcePath, String targetPath) async { - final sourceFile = _fileFor(sourcePath); - final targetFile = _fileFor(targetPath); - if (!await sourceFile.exists()) { - throw FileSystemException('Source file does not exist', sourcePath); - } - await targetFile.parent.create(recursive: true); - await sourceFile.copy(targetFile.path); - final sourceMeta = _metaFileFor(sourcePath); - final targetMeta = _metaFileFor(targetPath); - if (await sourceMeta.exists()) { - await sourceMeta.copy(targetMeta.path); + Future clear() async { + if (await baseDir.exists()) { + await baseDir.delete(recursive: true); } + await baseDir.create(recursive: true); } } diff --git a/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart b/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart index 8b9687c7..b3f97e1b 100644 --- a/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart +++ b/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart @@ -35,14 +35,13 @@ class SyncingService { final AbstractRemoteStorageAdapter remoteStorage; final AbstractLocalStorageAdapter localStorage; final AbstractAttachmentService attachmentsService; - final Future Function(String) getLocalUri; final AbstractSyncErrorHandler? errorHandler; final Duration syncThrottle; final Duration period; final Logger logger; - StreamSubscription? _syncSubscription; - StreamSubscription? _periodicSubscription; + StreamSubscription? _syncSubscription; + StreamSubscription? _periodicSubscription; bool _isClosed = false; final _syncTriggerController = StreamController.broadcast(); @@ -50,7 +49,6 @@ class SyncingService { required this.remoteStorage, required this.localStorage, required this.attachmentsService, - required this.getLocalUri, this.errorHandler, this.syncThrottle = const Duration(seconds: 5), this.period = const Duration(seconds: 30), @@ -71,8 +69,9 @@ class SyncingService { final manualTriggers = _syncTriggerController.stream; // Merge both streams and apply throttling - final mergedStream = StreamGroup.merge([attachmentChanges, manualTriggers]) - .transform(_throttleTransformer(syncThrottle)) + final mergedStream = + StreamGroup.merge([attachmentChanges, manualTriggers]) + .transform(_throttleTransformer(syncThrottle)) .listen((_) async { try { await attachmentsService.withContext((context) async { @@ -99,7 +98,8 @@ class SyncingService { _syncSubscription = mergedStream; // Start periodic sync - _periodicSubscription = Stream.periodic(period).listen((_) { + _periodicSubscription = + Stream.periodic(period, (_) => null).listen((_) { logger.info('Periodically syncing attachments'); triggerSync(); }); @@ -110,7 +110,7 @@ class SyncingService { handleData: (data, sink) { sink.add(data); // Simple throttle implementation - just delay the next event - Future.delayed(throttle); + Future.delayed(throttle); }, ); } @@ -244,7 +244,7 @@ class SyncingService { logger.info( 'Starting download for attachment ${attachment.id}', ); - final attachmentPath = await getLocalUri(attachment.filename); + final attachmentPath = attachment.filename; try { final fileStream = await remoteStorage.downloadFile(attachment); await localStorage.saveFile( diff --git a/packages/powersync_attachments_stream/test/local_storage_test.dart b/packages/powersync_attachments_stream/test/local_storage_test.dart index 2d75f32a..fb1bfc49 100644 --- a/packages/powersync_attachments_stream/test/local_storage_test.dart +++ b/packages/powersync_attachments_stream/test/local_storage_test.dart @@ -11,7 +11,7 @@ void main() { late IOLocalStorage storage; setUp(() async { - storage = IOLocalStorage(Directory(d.sandbox)); + storage = IOLocalStorage(d.sandbox); }); tearDown(() async { @@ -111,6 +111,98 @@ void main() { }); }); + group('edge cases and robustness', () { + test('saveFile with empty stream writes empty file and returns 0 size', () async { + const filePath = 'empty_file'; + final emptyStream = Stream.fromIterable(const []); + + final size = await storage.saveFile(filePath, emptyStream); + expect(size, 0); + + final resultStream = storage.readFile(filePath); + final chunks = await resultStream.toList(); + expect(chunks, isEmpty); + + final file = File(p.join(d.sandbox, filePath)); + expect(await file.exists(), isTrue); + expect(await file.length(), 0); + }); + + test('readFile preserves byte order (chunking may differ)', () async { + const filePath = 'ordered_chunks'; + final chunks = [ + Uint8List.fromList([0, 1, 2]), + Uint8List.fromList([3, 4]), + Uint8List.fromList([5, 6, 7, 8]), + ]; + await storage.saveFile(filePath, Stream.fromIterable(chunks)); + + final outChunks = await storage.readFile(filePath).toList(); + final outBytes = Uint8List.fromList( + outChunks.expand((c) => c).toList(), + ); + final expectedBytes = Uint8List.fromList( + chunks.expand((c) => c).toList(), + ); + expect(outBytes, equals(expectedBytes)); + }); + + test('fileExists becomes false after deleteFile', () async { + const filePath = 'exists_then_delete'; + await storage.saveFile(filePath, Stream.value(Uint8List.fromList([1]))); + expect(await storage.fileExists(filePath), isTrue); + await storage.deleteFile(filePath); + expect(await storage.fileExists(filePath), isFalse); + }); + + test('initialize is idempotent', () async { + await storage.initialize(); + await storage.initialize(); + + // Create a file, then re-initialize again + const filePath = 'idempotent_test'; + await storage.saveFile(filePath, Stream.value(Uint8List.fromList([9]))); + await storage.initialize(); + + // File should still exist (initialize should not clear data) + expect(await storage.fileExists(filePath), isTrue); + }); + + test('clear works even if base directory was removed externally', () async { + await storage.initialize(); + + // Remove the base dir manually + final baseDir = Directory(d.sandbox); + if (await baseDir.exists()) { + await baseDir.delete(recursive: true); + } + + // Calling clear should recreate base dir + await storage.clear(); + expect(await baseDir.exists(), isTrue); + }); + + test('supports unicode and emoji filenames', () async { + const filePath = '測試_файл_📷.bin'; + final bytes = Uint8List.fromList([10, 20, 30, 40]); + await storage.saveFile(filePath, Stream.value(bytes)); + + final out = await storage.readFile(filePath).toList(); + expect(out, equals([bytes])); + + await d.file(filePath, bytes).validate(); + }); + + test('readFile accepts mediaType parameter (ignored by IO impl)', () async { + const filePath = 'with_media_type'; + final data = Uint8List.fromList([1, 2, 3]); + await storage.saveFile(filePath, Stream.value(data)); + + final result = await storage.readFile(filePath, mediaType: 'image/jpeg').toList(); + expect(result, equals([data])); + }); + }); + group('deleteFile', () { test('deletes existing file', () async { const filePath = 'delete_test'; @@ -134,124 +226,48 @@ void main() { }); }); - group('fileExists', () { - test('returns true for existing file', () async { - const filePath = 'exists_test'; - final data = Uint8List.fromList([1, 2, 3]); - final dataStream = Stream.fromIterable([data]); + group('initialize and clear', () { + test('initialize creates the base directory', () async { + final newStorage = IOLocalStorage(p.join(d.sandbox, 'new_dir')); + final baseDir = Directory(p.join(d.sandbox, 'new_dir')); - await storage.saveFile(filePath, dataStream); - expect(await storage.fileExists(filePath), isTrue); + expect(await baseDir.exists(), isFalse); - await d.file(filePath, data).validate(); - }); + await newStorage.initialize(); - test('returns false for non-existent file', () async { - const filePath = 'non_existent'; - expect(await storage.fileExists(filePath), isFalse); - expect(await File(p.join(d.sandbox, filePath)).exists(), isFalse); + expect(await baseDir.exists(), isTrue); }); - }); - group('makeDir', () { - test('creates directory and its parents', () async { - const dirPath = 'test_dir/subdir'; - final fullPath = Directory(p.join(d.sandbox, dirPath)); + test('clear removes and recreates the base directory', () async { + await storage.initialize(); + final testFile = p.join(d.sandbox, 'test_file'); + await File(testFile).writeAsString('test'); - expect(await fullPath.exists(), isFalse); - await storage.makeDir(dirPath); - expect(await fullPath.exists(), isTrue); + expect(await File(testFile).exists(), isTrue); - await d.dir('test_dir/subdir').validate(); - }); + await storage.clear(); - test('does not throw when directory already exists', () async { - const dirPath = 'existing_dir'; - await storage.makeDir(dirPath); - await storage.makeDir(dirPath); // Should not throw - expect(await Directory(p.join(d.sandbox, dirPath)).exists(), isTrue); - - await d.dir('existing_dir').validate(); + expect(await Directory(d.sandbox).exists(), isTrue); + expect(await File(testFile).exists(), isFalse); }); }); - group('rmDir', () { - test( - 'recursively deletes directory with files and subdirectories', - () async { - const dirPath = 'test_dir'; - final file1Path = p.join(dirPath, 'file1'); - final file2Path = p.join(dirPath, 'subdir/file2'); - final data = Uint8List.fromList([1, 2, 3]); - - await storage.saveFile(file1Path, Stream.fromIterable([data])); - await storage.saveFile(file2Path, Stream.fromIterable([data])); - - final dir = Directory(p.join(d.sandbox, dirPath)); - expect(await dir.exists(), isTrue); - - await storage.rmDir(dirPath); - expect(await dir.exists(), isFalse); - - // Assert directory does not exist - expect(await Directory(p.join(d.sandbox, dirPath)).exists(), isFalse); - }, - ); - - test('does not throw when directory does not exist', () async { - const dirPath = 'non_existent_dir'; - await storage.rmDir(dirPath); - expect(await Directory(p.join(d.sandbox, dirPath)).exists(), isFalse); - }); - }); - - group('copyFile', () { - test('copies file to target path', () async { - const sourcePath = 'source_file'; - const targetPath = 'target_file'; + group('fileExists', () { + test('returns true for existing file', () async { + const filePath = 'exists_test'; final data = Uint8List.fromList([1, 2, 3]); final dataStream = Stream.fromIterable([data]); - await storage.saveFile(sourcePath, dataStream); - await storage.copyFile(sourcePath, targetPath); - - final resultStream = storage.readFile(targetPath); - final result = await resultStream.toList(); - expect(result, equals([data])); - expect(await storage.fileExists(sourcePath), isTrue); - - await d.file(targetPath, data).validate(); - await d.file(sourcePath, data).validate(); - }); + await storage.saveFile(filePath, dataStream); + expect(await storage.fileExists(filePath), isTrue); - test('throws when source file does not exist', () async { - const sourcePath = 'non_existent'; - const targetPath = 'target'; - expect( - () => storage.copyFile(sourcePath, targetPath), - throwsA(isA()), - ); - expect(await File(p.join(d.sandbox, targetPath)).exists(), isFalse); + await d.file(filePath, data).validate(); }); - test('creates target parent directories', () async { - const sourcePath = 'source_file'; - const targetPath = 'subdir/nested/target_file'; - final data = Uint8List.fromList([1, 2, 3]); - final dataStream = Stream.fromIterable([data]); - - await storage.saveFile(sourcePath, dataStream); - await storage.copyFile(sourcePath, targetPath); - - final resultStream = storage.readFile(targetPath); - final result = await resultStream.toList(); - expect(result, equals([data])); - expect( - await Directory(p.join(d.sandbox, 'subdir', 'nested')).exists(), - isTrue, - ); - - await d.dir('subdir/nested', [d.file('target_file', data)]).validate(); + test('returns false for non-existent file', () async { + const filePath = 'non_existent'; + expect(await storage.fileExists(filePath), isFalse); + expect(await File(p.join(d.sandbox, filePath)).exists(), isFalse); }); }); From afeec6eec246a70e3eb6524d2ec8afa92ad119b4 Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Tue, 12 Aug 2025 14:50:04 +0200 Subject: [PATCH 12/18] Refactor logger variable name in attachment queue initialization for consistency. Removed outdated comment regarding attachments directory in attachment queue service documentation. --- demos/supabase-todolist/lib/attachments/queue.dart | 5 ++--- .../lib/src/attachment_queue_service.dart | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/demos/supabase-todolist/lib/attachments/queue.dart b/demos/supabase-todolist/lib/attachments/queue.dart index 7dcd7b29..179c104f 100644 --- a/demos/supabase-todolist/lib/attachments/queue.dart +++ b/demos/supabase-todolist/lib/attachments/queue.dart @@ -9,7 +9,7 @@ import 'package:powersync_flutter_demo/attachments/remote_storage_adapter.dart'; late AttachmentQueue attachmentQueue; final remoteStorage = SupabaseStorageAdapter(); -final log = Logger('AttachmentQueue'); +final logger = Logger('AttachmentQueue'); Future initializeAttachmentQueue(PowerSyncDatabase db) async { // Use the app's document directory for local storage @@ -18,9 +18,8 @@ Future initializeAttachmentQueue(PowerSyncDatabase db) async { attachmentQueue = AttachmentQueue( db: db, remoteStorage: remoteStorage, - logger: log, + logger: logger, localStorage: IOLocalStorage(appDocDir.path), - // attachmentsDirectory: '${appDocDir.path}/attachments', watchAttachments: () => db.watch(''' SELECT photo_id as id FROM todos WHERE photo_id IS NOT NULL ''').map((results) => results diff --git a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart index bc81db0f..68a6dcae 100644 --- a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart +++ b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart @@ -61,7 +61,6 @@ class WatchedAttachmentItem { /// Properties: /// - [db]: PowerSync database client. /// - [remoteStorage]: Adapter which interfaces with the remote storage backend. -/// - [attachmentsDirectory]: Directory name where attachment files will be written to disk. /// - [watchAttachments]: A stream generator for the current state of local attachments. /// - [localStorage]: Provides access to local filesystem storage methods. /// - [attachmentsQueueTableName]: SQLite table where attachment state will be recorded. From 0d6a55785a30d90bb72470075a0a819d631ab515 Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Tue, 12 Aug 2025 15:09:04 +0200 Subject: [PATCH 13/18] Removed unecessary comment --- .../lib/src/attachment_queue_service.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart index 68a6dcae..785a8ac8 100644 --- a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart +++ b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart @@ -223,8 +223,7 @@ class AttachmentQueue { id: item.id, filename: filename, state: AttachmentState.queuedDownload, - metaData: item.metaData, - // hasSynced: existingQueueItem.hasSynced; + metaData: item.metaData ), ); } else if (existingQueueItem.state == AttachmentState.archived) { From 5bea0b91b9a8a5ea0a44d799640652a394b207ee Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Wed, 13 Aug 2025 09:32:26 +0200 Subject: [PATCH 14/18] Added attachment queue service as an export in common.dart --- packages/powersync_attachments_stream/lib/common.dart | 3 ++- .../lib/src/attachment_queue_service.dart | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/powersync_attachments_stream/lib/common.dart b/packages/powersync_attachments_stream/lib/common.dart index 9c8bd8bd..e83f6928 100644 --- a/packages/powersync_attachments_stream/lib/common.dart +++ b/packages/powersync_attachments_stream/lib/common.dart @@ -4,4 +4,5 @@ export 'src/abstractions/sync_error_handler.dart'; export 'src/abstractions/attachment_service.dart'; export 'src/abstractions/attachment_context.dart'; export 'src/abstractions/local_storage.dart'; -export 'src/abstractions/remote_storage.dart'; \ No newline at end of file +export 'src/abstractions/remote_storage.dart'; +export 'src/attachment_queue_service.dart'; \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart index 785a8ac8..9081f63e 100644 --- a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart +++ b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart @@ -223,7 +223,7 @@ class AttachmentQueue { id: item.id, filename: filename, state: AttachmentState.queuedDownload, - metaData: item.metaData + metaData: item.metaData, ), ); } else if (existingQueueItem.state == AttachmentState.archived) { From 4a2721a3ec123fb1abf8d20f6cc54c5513d09ac6 Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Wed, 13 Aug 2025 16:05:07 +0200 Subject: [PATCH 15/18] Refactor photo attachment handling in the Supabase Todo List demo. Updated photo capture widget to save attachments as byte data instead of streams, and modified related methods in the attachment queue service to accept byte data. Updated tests to reflect changes in data handling. --- .../lib/attachments/photo_capture_widget.dart | 9 +- .../lib/attachments/photo_widget.dart | 1 + .../lib/attachments/queue.dart | 4 +- .../src/abstractions/attachment_service.dart | 2 +- .../lib/src/abstractions/local_storage.dart | 4 +- .../lib/src/abstractions/remote_storage.dart | 1 + .../lib/src/attachment.dart | 1 + .../lib/src/attachment_queue_service.dart | 7 +- .../implementations/attachment_context.dart | 5 +- .../implementations/attachment_service.dart | 6 +- .../lib/src/storage/io_local_storage.dart | 33 +------ .../lib/src/sync/syncing_service.dart | 95 +++++++------------ .../test/local_storage_test.dart | 58 +++++------ 13 files changed, 86 insertions(+), 140 deletions(-) diff --git a/demos/supabase-todolist/lib/attachments/photo_capture_widget.dart b/demos/supabase-todolist/lib/attachments/photo_capture_widget.dart index 61d26ea6..86fa7d76 100644 --- a/demos/supabase-todolist/lib/attachments/photo_capture_widget.dart +++ b/demos/supabase-todolist/lib/attachments/photo_capture_widget.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:io'; -import 'dart:typed_data'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; @@ -47,17 +46,17 @@ class _TakePhotoWidgetState extends State { await _initializeControllerFuture; final XFile photo = await _cameraController.takePicture(); - // Read the photo data as a stream + // Read the photo data as bytes final photoFile = File(photo.path); if (!await photoFile.exists()) { log.warning('Photo file does not exist: ${photo.path}'); return; } - final photoDataStream = photoFile.openRead().cast(); + final photoData = await photoFile.readAsBytes(); - // Save the photo attachment directly with the data stream - final attachment = await savePhotoAttachment(photoDataStream, widget.todoId); + // Save the photo attachment with the byte data + final attachment = await savePhotoAttachment(photoData, widget.todoId); log.info('Photo attachment saved with ID: ${attachment.id}'); } catch (e) { diff --git a/demos/supabase-todolist/lib/attachments/photo_widget.dart b/demos/supabase-todolist/lib/attachments/photo_widget.dart index ecf5a94e..2683008f 100644 --- a/demos/supabase-todolist/lib/attachments/photo_widget.dart +++ b/demos/supabase-todolist/lib/attachments/photo_widget.dart @@ -1,4 +1,5 @@ import 'dart:io'; + import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as p; import 'package:flutter/material.dart'; diff --git a/demos/supabase-todolist/lib/attachments/queue.dart b/demos/supabase-todolist/lib/attachments/queue.dart index 179c104f..4a5375b1 100644 --- a/demos/supabase-todolist/lib/attachments/queue.dart +++ b/demos/supabase-todolist/lib/attachments/queue.dart @@ -1,6 +1,6 @@ import 'dart:async'; import 'dart:io'; -import 'dart:typed_data'; + import 'package:logging/logging.dart'; import 'package:path_provider/path_provider.dart'; import 'package:powersync/powersync.dart'; @@ -34,7 +34,7 @@ Future initializeAttachmentQueue(PowerSyncDatabase db) async { } Future savePhotoAttachment( - Stream photoData, String todoId, + List photoData, String todoId, {String mediaType = 'image/jpeg'}) async { // Save the file using the AttachmentQueue API return await attachmentQueue.saveFile( diff --git a/packages/powersync_attachments_stream/lib/src/abstractions/attachment_service.dart b/packages/powersync_attachments_stream/lib/src/abstractions/attachment_service.dart index d8d26d9c..3873a648 100644 --- a/packages/powersync_attachments_stream/lib/src/abstractions/attachment_service.dart +++ b/packages/powersync_attachments_stream/lib/src/abstractions/attachment_service.dart @@ -4,7 +4,7 @@ import 'attachment_context.dart'; abstract class AbstractAttachmentService { /// Watcher for changes to attachments table. /// Once a change is detected it will initiate a sync of the attachments. - Stream watchActiveAttachments(); + Stream watchActiveAttachments({Duration? throttle}); /// Executes a callback with an exclusive lock on all attachment operations. /// This helps prevent race conditions between different updates. diff --git a/packages/powersync_attachments_stream/lib/src/abstractions/local_storage.dart b/packages/powersync_attachments_stream/lib/src/abstractions/local_storage.dart index ddc85b8b..a978d51f 100644 --- a/packages/powersync_attachments_stream/lib/src/abstractions/local_storage.dart +++ b/packages/powersync_attachments_stream/lib/src/abstractions/local_storage.dart @@ -4,9 +4,9 @@ abstract class AbstractLocalStorageAdapter { /// Saves binary data stream to storage at the specified file path /// /// [filePath] - Path where the file will be stored - /// [data] - Stream of binary data to store + /// [data] - List of binary data to store /// Returns the total size of the written data in bytes - Future saveFile(String filePath, Stream data); + Future saveFile(String filePath, List data); /// Retrieves binary data stream from storage at the specified file path /// diff --git a/packages/powersync_attachments_stream/lib/src/abstractions/remote_storage.dart b/packages/powersync_attachments_stream/lib/src/abstractions/remote_storage.dart index 80256a2f..d07122a8 100644 --- a/packages/powersync_attachments_stream/lib/src/abstractions/remote_storage.dart +++ b/packages/powersync_attachments_stream/lib/src/abstractions/remote_storage.dart @@ -1,4 +1,5 @@ import 'dart:async'; + import '../attachment.dart'; /// Adapter for interfacing with remote attachment storage. diff --git a/packages/powersync_attachments_stream/lib/src/attachment.dart b/packages/powersync_attachments_stream/lib/src/attachment.dart index 62e3eef4..ec5ec0d5 100644 --- a/packages/powersync_attachments_stream/lib/src/attachment.dart +++ b/packages/powersync_attachments_stream/lib/src/attachment.dart @@ -4,6 +4,7 @@ import 'package:powersync_core/sqlite3_common.dart' show Row; import 'package:powersync_core/powersync_core.dart'; + import './attachment_queue_service.dart'; /// Represents the state of an attachment. diff --git a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart index 9081f63e..102a5a11 100644 --- a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart +++ b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart @@ -5,10 +5,12 @@ // It provides hooks for error handling, cache management, and custom filename resolution. import 'dart:async'; -import 'dart:typed_data'; + import 'package:logging/logging.dart'; import 'package:powersync_core/powersync_core.dart'; import 'package:sqlite_async/mutex.dart'; + + import 'attachment.dart'; import 'abstractions/attachment_service.dart'; import 'abstractions/attachment_context.dart'; @@ -265,7 +267,6 @@ class AttachmentQueue { attachment.copyWith(state: AttachmentState.archived), ); } - break; default: attachmentUpdates.add( attachment.copyWith(state: AttachmentState.archived), @@ -281,7 +282,7 @@ class AttachmentQueue { /// Creates a new attachment locally and queues it for upload. /// The filename is resolved using [resolveNewAttachmentFilename]. Future saveFile({ - required Stream data, + required List data, required String mediaType, String? fileExtension, String? metaData, diff --git a/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart b/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart index 8c7176af..1fd6edff 100644 --- a/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart +++ b/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart @@ -1,9 +1,10 @@ -import '../abstractions/attachment_context.dart'; -import '../attachment.dart'; import 'package:powersync_core/powersync_core.dart'; import 'package:powersync_core/sqlite3_common.dart'; import 'package:logging/logging.dart'; +import '../abstractions/attachment_context.dart'; +import '../attachment.dart'; + class AttachmentContextImpl implements AbstractAttachmentContext { final PowerSyncDatabase db; final Logger log; diff --git a/packages/powersync_attachments_stream/lib/src/implementations/attachment_service.dart b/packages/powersync_attachments_stream/lib/src/implementations/attachment_service.dart index 9cb263aa..50afa12f 100644 --- a/packages/powersync_attachments_stream/lib/src/implementations/attachment_service.dart +++ b/packages/powersync_attachments_stream/lib/src/implementations/attachment_service.dart @@ -1,4 +1,5 @@ import 'dart:async'; + import 'package:logging/logging.dart'; import 'package:powersync_core/powersync_core.dart'; import 'package:sqlite_async/sqlite_async.dart'; @@ -6,7 +7,7 @@ import 'package:sqlite_async/sqlite_async.dart'; import '../abstractions/attachment_service.dart'; import '../abstractions/attachment_context.dart'; import '../attachment.dart'; -import 'attachment_context.dart'; +import './attachment_context.dart'; class AttachmentServiceImpl implements AbstractAttachmentService { final PowerSyncDatabase db; @@ -32,7 +33,7 @@ class AttachmentServiceImpl implements AbstractAttachmentService { } @override - Stream watchActiveAttachments() async* { + Stream watchActiveAttachments({Duration? throttle}) async* { logger.info('Watching attachments...'); // Watch for attachments with active states (queued for upload, download, or delete) @@ -54,6 +55,7 @@ class AttachmentServiceImpl implements AbstractAttachmentService { AttachmentState.queuedDownload.index, AttachmentState.queuedDelete.index, ], + throttle: throttle ?? const Duration(milliseconds: 30), ); yield* stream; diff --git a/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart b/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart index 9a118178..8655641b 100644 --- a/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart +++ b/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart @@ -1,22 +1,9 @@ -/// Local storage adapter for handling file operations on the device filesystem. -/// -/// This file implements the [IOLocalStorage] class, which provides methods for -/// saving, reading, deleting, copying, and managing files and directories using -/// the Dart IO library. It is used as the local storage backend for attachments -/// in the PowerSync attachments system. -/// -/// Features: -/// - Save files from streams (creates directories and all necessary parents dynamically if they do not exist) -/// - Read files as streams -/// - Delete files and their metadata -/// - Copy files and their metadata -/// - Create and remove directories (creates all necessary parents dynamically) -/// - Check file existence - import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; + import 'package:path/path.dart' as p; + import '../abstractions/local_storage.dart'; /// Implements [AbstractLocalStorageAdapter] for device filesystem using Dart IO. @@ -38,21 +25,11 @@ class IOLocalStorage implements AbstractLocalStorageAdapter { /// Creates the file's directory and all necessary parent directories dynamically if they do not exist. /// Returns the total number of bytes written. @override - Future saveFile(String filePath, Stream data) async { + Future saveFile(String filePath, List data) async { final file = _fileFor(filePath); await file.parent.create(recursive: true); - var totalSize = 0; - final sink = file.openWrite(); - try { - await for (final chunk in data) { - sink.add(chunk); - totalSize += chunk.length; - } - await sink.flush(); - } finally { - await sink.close(); - } - return totalSize; + await file.writeAsBytes(data, flush: true); + return data.length; } /// Reads a file as a stream of [Uint8List] chunks. diff --git a/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart b/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart index b3f97e1b..3c4d5041 100644 --- a/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart +++ b/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart @@ -9,6 +9,7 @@ import 'dart:async'; import 'dart:typed_data'; + import 'package:logging/logging.dart'; import 'package:async/async.dart'; @@ -58,27 +59,29 @@ class SyncingService { /// Starts the syncing process, including periodic and event-driven sync operations. /// /// [period] is the interval at which periodic sync operations are triggered. - Future startSync({Duration period = const Duration(seconds: 30)}) async { + Future startSync({ + Duration period = const Duration(seconds: 30), + }) async { if (_isClosed) return; _syncSubscription?.cancel(); _periodicSubscription?.cancel(); // Create a merged stream of manual triggers and attachment changes - final attachmentChanges = attachmentsService.watchActiveAttachments(); + final attachmentChanges = attachmentsService.watchActiveAttachments( + throttle: syncThrottle, + ); final manualTriggers = _syncTriggerController.stream; // Merge both streams and apply throttling final mergedStream = - StreamGroup.merge([attachmentChanges, manualTriggers]) - .transform(_throttleTransformer(syncThrottle)) - .listen((_) async { + StreamGroup.merge([attachmentChanges, manualTriggers]).listen(( + _, + ) async { try { await attachmentsService.withContext((context) async { final attachments = await context.getActiveAttachments(); - logger.info( - 'Found ${attachments.length} active attachments', - ); + logger.info('Found ${attachments.length} active attachments'); await handleSync(attachments, context); await deleteArchivedAttachments(context); }); @@ -98,23 +101,14 @@ class SyncingService { _syncSubscription = mergedStream; // Start periodic sync - _periodicSubscription = - Stream.periodic(period, (_) => null).listen((_) { + _periodicSubscription = Stream.periodic(period, (_) => null).listen(( + _, + ) { logger.info('Periodically syncing attachments'); triggerSync(); }); } - StreamTransformer _throttleTransformer(Duration throttle) { - return StreamTransformer.fromHandlers( - handleData: (data, sink) { - sink.add(data); - // Simple throttle implementation - just delay the next event - Future.delayed(throttle); - }, - ); - } - /// Enqueues a sync operation (manual trigger). Future triggerSync() async { if (_isClosed) return; @@ -143,9 +137,7 @@ class SyncingService { List attachments, AbstractAttachmentContext context, ) async { - logger.info( - 'Starting handleSync with ${attachments.length} attachments', - ); + logger.info('Starting handleSync with ${attachments.length} attachments'); final updatedAttachments = []; for (final attachment in attachments) { @@ -167,29 +159,19 @@ class SyncingService { updatedAttachments.add(await deleteAttachment(attachment)); break; case AttachmentState.synced: - logger.info( - 'Attachment ${attachment.id} is already synced', - ); + logger.info('Attachment ${attachment.id} is already synced'); break; case AttachmentState.archived: - logger.info( - 'Attachment ${attachment.id} is archived', - ); + logger.info('Attachment ${attachment.id} is archived'); break; } } catch (e, st) { - logger.warning( - 'Error during sync for ${attachment.id}', - e, - st, - ); + logger.warning('Error during sync for ${attachment.id}', e, st); } } if (updatedAttachments.isNotEmpty) { - logger.info( - 'Saving ${updatedAttachments.length} updated attachments', - ); + logger.info('Saving ${updatedAttachments.length} updated attachments'); await context.saveAttachments(updatedAttachments); } } @@ -199,9 +181,7 @@ class SyncingService { /// [attachment]: The attachment to upload. /// Returns the updated attachment with its new state. Future uploadAttachment(Attachment attachment) async { - logger.info( - 'Starting upload for attachment ${attachment.id}', - ); + logger.info('Starting upload for attachment ${attachment.id}'); try { if (attachment.localUri == null) { throw Exception('No localUri for attachment $attachment'); @@ -226,9 +206,7 @@ class SyncingService { if (errorHandler != null) { final shouldRetry = await errorHandler!.onUploadError(attachment, e); if (!shouldRetry) { - logger.info( - 'Attachment with ID ${attachment.id} has been archived', - ); + logger.info('Attachment with ID ${attachment.id} has been archived'); return attachment.copyWith(state: AttachmentState.archived); } } @@ -241,19 +219,18 @@ class SyncingService { /// [attachment]: The attachment to download. /// Returns the updated attachment with its new state. Future downloadAttachment(Attachment attachment) async { - logger.info( - 'Starting download for attachment ${attachment.id}', - ); + logger.info('Starting download for attachment ${attachment.id}'); final attachmentPath = attachment.filename; try { final fileStream = await remoteStorage.downloadFile(attachment); - await localStorage.saveFile( - attachmentPath, - fileStream.map((chunk) => Uint8List.fromList(chunk)), - ); - logger.info( - 'Successfully downloaded file "${attachment.id}"', - ); + // Collect the stream into a single List to satisfy localStorage API. + final builder = BytesBuilder(copy: false); + await for (final chunk in fileStream) { + builder.add(chunk); + } + final bytes = builder.takeBytes(); + await localStorage.saveFile(attachmentPath, bytes); + logger.info('Successfully downloaded file "${attachment.id}"'); return attachment.copyWith( localUri: attachmentPath, @@ -264,9 +241,7 @@ class SyncingService { if (errorHandler != null) { final shouldRetry = await errorHandler!.onDownloadError(attachment, e); if (!shouldRetry) { - logger.info( - 'Attachment with ID ${attachment.id} has been archived', - ); + logger.info('Attachment with ID ${attachment.id} has been archived'); return attachment.copyWith(state: AttachmentState.archived); } } @@ -285,9 +260,7 @@ class SyncingService { /// Returns the updated attachment with its new state. Future deleteAttachment(Attachment attachment) async { try { - logger.info( - 'Deleting attachment ${attachment.id} from remote storage', - ); + logger.info('Deleting attachment ${attachment.id} from remote storage'); await remoteStorage.deleteFile(attachment); if (attachment.localUri != null && @@ -312,7 +285,9 @@ class SyncingService { /// /// [context]: The attachment context used to retrieve and manage archived attachments. /// Returns `true` if all archived attachments were successfully deleted, `false` otherwise. - Future deleteArchivedAttachments(AbstractAttachmentContext context) async { + Future deleteArchivedAttachments( + AbstractAttachmentContext context, + ) async { return context.deleteArchivedAttachments((pendingDelete) async { for (final attachment in pendingDelete) { if (attachment.localUri == null) continue; diff --git a/packages/powersync_attachments_stream/test/local_storage_test.dart b/packages/powersync_attachments_stream/test/local_storage_test.dart index fb1bfc49..73a2a7ec 100644 --- a/packages/powersync_attachments_stream/test/local_storage_test.dart +++ b/packages/powersync_attachments_stream/test/local_storage_test.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; + import 'package:test/test.dart'; import 'package:path/path.dart' as p; import 'package:powersync_attachments_stream/src/storage/io_local_storage.dart'; @@ -20,12 +21,10 @@ void main() { }); group('saveFile and readFile', () { - test('saves and reads binary data stream successfully', () async { + test('saves and reads binary data successfully', () async { const filePath = 'test_file'; final data = Uint8List.fromList([1, 2, 3, 4, 5]); - final dataStream = Stream.fromIterable([data]); - - final size = await storage.saveFile(filePath, dataStream); + final size = await storage.saveFile(filePath, data); expect(size, equals(data.length)); final resultStream = storage.readFile(filePath); @@ -51,11 +50,10 @@ void main() { const filePath = 'subdir/nested/test'; final nonExistentDir = Directory(p.join(d.sandbox, 'subdir', 'nested')); final data = Uint8List.fromList([1, 2, 3]); - final dataStream = Stream.fromIterable([data]); expect(await nonExistentDir.exists(), isFalse); - final size = await storage.saveFile(filePath, dataStream); + final size = await storage.saveFile(filePath, data); expect(size, equals(data.length)); expect(await nonExistentDir.exists(), isTrue); @@ -73,11 +71,10 @@ void main() { p.join(d.sandbox, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'), ); final data = Uint8List.fromList([42, 43, 44]); - final dataStream = Stream.fromIterable([data]); expect(await nestedDir.exists(), isFalse); - final size = await storage.saveFile(filePath, dataStream); + final size = await storage.saveFile(filePath, data); expect(size, equals(data.length)); expect(await nestedDir.exists(), isTrue); @@ -95,11 +92,9 @@ void main() { const filePath = 'overwrite_test'; final originalData = Uint8List.fromList([1, 2, 3]); final newData = Uint8List.fromList([4, 5, 6, 7]); - final originalStream = Stream.fromIterable([originalData]); - final newStream = Stream.fromIterable([newData]); - await storage.saveFile(filePath, originalStream); - final size = await storage.saveFile(filePath, newStream); + await storage.saveFile(filePath, originalData); + final size = await storage.saveFile(filePath, newData); expect(size, equals(newData.length)); final resultStream = storage.readFile(filePath); @@ -112,11 +107,11 @@ void main() { }); group('edge cases and robustness', () { - test('saveFile with empty stream writes empty file and returns 0 size', () async { + test('saveFile with empty data writes empty file and returns 0 size', () async { const filePath = 'empty_file'; - final emptyStream = Stream.fromIterable(const []); + final emptyBytes = Uint8List(0); - final size = await storage.saveFile(filePath, emptyStream); + final size = await storage.saveFile(filePath, emptyBytes); expect(size, 0); final resultStream = storage.readFile(filePath); @@ -135,21 +130,19 @@ void main() { Uint8List.fromList([3, 4]), Uint8List.fromList([5, 6, 7, 8]), ]; - await storage.saveFile(filePath, Stream.fromIterable(chunks)); + final expectedBytes = Uint8List.fromList(chunks.expand((c) => c).toList()); + await storage.saveFile(filePath, expectedBytes); final outChunks = await storage.readFile(filePath).toList(); final outBytes = Uint8List.fromList( outChunks.expand((c) => c).toList(), ); - final expectedBytes = Uint8List.fromList( - chunks.expand((c) => c).toList(), - ); expect(outBytes, equals(expectedBytes)); }); test('fileExists becomes false after deleteFile', () async { const filePath = 'exists_then_delete'; - await storage.saveFile(filePath, Stream.value(Uint8List.fromList([1]))); + await storage.saveFile(filePath, Uint8List.fromList([1])); expect(await storage.fileExists(filePath), isTrue); await storage.deleteFile(filePath); expect(await storage.fileExists(filePath), isFalse); @@ -161,7 +154,7 @@ void main() { // Create a file, then re-initialize again const filePath = 'idempotent_test'; - await storage.saveFile(filePath, Stream.value(Uint8List.fromList([9]))); + await storage.saveFile(filePath, Uint8List.fromList([9])); await storage.initialize(); // File should still exist (initialize should not clear data) @@ -185,7 +178,7 @@ void main() { test('supports unicode and emoji filenames', () async { const filePath = '測試_файл_📷.bin'; final bytes = Uint8List.fromList([10, 20, 30, 40]); - await storage.saveFile(filePath, Stream.value(bytes)); + await storage.saveFile(filePath, bytes); final out = await storage.readFile(filePath).toList(); expect(out, equals([bytes])); @@ -196,7 +189,7 @@ void main() { test('readFile accepts mediaType parameter (ignored by IO impl)', () async { const filePath = 'with_media_type'; final data = Uint8List.fromList([1, 2, 3]); - await storage.saveFile(filePath, Stream.value(data)); + await storage.saveFile(filePath, data); final result = await storage.readFile(filePath, mediaType: 'image/jpeg').toList(); expect(result, equals([data])); @@ -207,9 +200,8 @@ void main() { test('deletes existing file', () async { const filePath = 'delete_test'; final data = Uint8List.fromList([1, 2, 3]); - final dataStream = Stream.fromIterable([data]); - await storage.saveFile(filePath, dataStream); + await storage.saveFile(filePath, data); expect(await storage.fileExists(filePath), isTrue); await storage.deleteFile(filePath); @@ -256,9 +248,8 @@ void main() { test('returns true for existing file', () async { const filePath = 'exists_test'; final data = Uint8List.fromList([1, 2, 3]); - final dataStream = Stream.fromIterable([data]); - await storage.saveFile(filePath, dataStream); + await storage.saveFile(filePath, data); expect(await storage.fileExists(filePath), isTrue); await d.file(filePath, data).validate(); @@ -275,9 +266,8 @@ void main() { test('handles special characters in file path', () async { const filePath = 'file with spaces & symbols!@#'; final data = Uint8List.fromList([1, 2, 3]); - final dataStream = Stream.fromIterable([data]); - final size = await storage.saveFile(filePath, dataStream); + final size = await storage.saveFile(filePath, data); expect(size, equals(data.length)); final resultStream = storage.readFile(filePath); @@ -302,9 +292,7 @@ void main() { ), ); } - final dataStream = Stream.fromIterable(chunks); - - final size = await storage.saveFile(filePath, dataStream); + final size = await storage.saveFile(filePath, data); expect(size, equals(data.length)); final resultStream = storage.readFile(filePath); @@ -324,7 +312,7 @@ void main() { for (int i = 0; i < fileCount; i++) { final data = Uint8List.fromList([i, i + 1, i + 2]); - futures.add(storage.saveFile('file_$i', Stream.fromIterable([data]))); + futures.add(storage.saveFile('file_$i', data)); } await Future.wait(futures); @@ -349,8 +337,8 @@ void main() { final data1 = Uint8List.fromList([1, 2, 3]); final data2 = Uint8List.fromList([4, 5, 6]); final futures = [ - storage.saveFile(filePath, Stream.fromIterable([data1])), - storage.saveFile(filePath, Stream.fromIterable([data2])), + storage.saveFile(filePath, data1), + storage.saveFile(filePath, data2), ]; await Future.wait(futures); From e041fcd6ef1b95fe3ed3ea8770ba91ed3241b6b7 Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Wed, 13 Aug 2025 16:25:36 +0200 Subject: [PATCH 16/18] Updated documentation in SyncingService to streamline error handling comments, replacing multiple error handler parameters with a single optional error handler for sync-related errors. --- .../lib/src/sync/syncing_service.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart b/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart index 3c4d5041..362e0f53 100644 --- a/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart +++ b/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart @@ -30,8 +30,7 @@ import '../abstractions/sync_error_handler.dart'; /// - [remoteStorage]: The remote storage implementation for handling file operations. /// - [localStorage]: The local storage implementation for managing files locally. /// - [attachmentsService]: The service for managing attachment states and operations. -/// - [getLocalUri]: A function to resolve the local URI for a given filename. -/// - [onDownloadError], [onUploadError], [onDeleteError]: Optional error handlers for managing sync-related errors. +/// - [errorHandler]: Optional error handler for managing sync-related errors. class SyncingService { final AbstractRemoteStorageAdapter remoteStorage; final AbstractLocalStorageAdapter localStorage; From 126dadfdaa220b9475133a83768d81cf6cd67ab7 Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Thu, 14 Aug 2025 10:51:05 +0200 Subject: [PATCH 17/18] Remoed dynamic variables, implemented context.deleteAttachment() and fixed various formatting --- .../lib/widgets/todo_item_widget.dart | 2 +- demos/supabase-todolist/pubspec.lock | 13 ++--- demos/supabase-todolist/pubspec.yaml | 3 +- .../powersync_attachments_stream/.metadata | 10 ---- .../lib/common.dart | 2 +- .../lib/powersync_attachments_stream.dart | 1 - .../src/abstractions/attachment_context.dart | 13 +++-- .../src/abstractions/attachment_service.dart | 5 +- .../lib/src/abstractions/local_storage.dart | 2 +- .../lib/src/abstractions/remote_storage.dart | 2 +- .../src/abstractions/sync_error_handler.dart | 4 +- .../lib/src/attachment.dart | 38 ++++++++------ .../lib/src/attachment_queue_service.dart | 37 ++++++-------- .../implementations/attachment_context.dart | 16 +++--- .../lib/src/storage/io_local_storage.dart | 4 +- .../lib/src/sync/syncing_service.dart | 50 ++++++++++--------- .../powersync_attachments_stream/pubspec.lock | 8 +-- .../powersync_attachments_stream/pubspec.yaml | 3 +- .../test/local_storage_test.dart | 19 ++++--- 19 files changed, 116 insertions(+), 116 deletions(-) delete mode 100644 packages/powersync_attachments_stream/.metadata diff --git a/demos/supabase-todolist/lib/widgets/todo_item_widget.dart b/demos/supabase-todolist/lib/widgets/todo_item_widget.dart index c8d82a5f..ce5b3575 100644 --- a/demos/supabase-todolist/lib/widgets/todo_item_widget.dart +++ b/demos/supabase-todolist/lib/widgets/todo_item_widget.dart @@ -27,7 +27,7 @@ class TodoItemWidget extends StatelessWidget { await attachmentQueue.deleteFile( attachmentId: todo.photoId!, updateHook: (context, attachment) async { - // await context.execute("UPDATE todos SET photo_id = NULL WHERE id = ?", [todo.id]); + await context.execute("UPDATE todos SET photo_id = NULL WHERE id = ?", [todo.id]); }, ); } diff --git a/demos/supabase-todolist/pubspec.lock b/demos/supabase-todolist/pubspec.lock index 0237ed2a..7f0b426d 100644 --- a/demos/supabase-todolist/pubspec.lock +++ b/demos/supabase-todolist/pubspec.lock @@ -567,13 +567,6 @@ packages: relative: true source: path version: "1.15.0" - powersync_attachments_helper: - dependency: "direct overridden" - description: - path: "../../packages/powersync_attachments_helper" - relative: true - source: path - version: "0.6.18+11" powersync_attachments_stream: dependency: "direct main" description: @@ -764,10 +757,10 @@ packages: dependency: transitive description: name: sqlite3 - sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e" + sha256: f393d92c71bdcc118d6203d07c991b9be0f84b1a6f89dd4f7eed348131329924 url: "https://pub.dev" source: hosted - version: "2.7.5" + version: "2.9.0" sqlite3_flutter_libs: dependency: transitive description: @@ -1057,5 +1050,5 @@ packages: source: hosted version: "2.1.0" sdks: - dart: ">=3.8.1 <4.0.0" + dart: ">=3.7.0 <4.0.0" flutter: ">=3.27.0" diff --git a/demos/supabase-todolist/pubspec.yaml b/demos/supabase-todolist/pubspec.yaml index abd5987b..6829a0d4 100644 --- a/demos/supabase-todolist/pubspec.yaml +++ b/demos/supabase-todolist/pubspec.yaml @@ -10,8 +10,7 @@ environment: dependencies: flutter: sdk: flutter - powersync_attachments_stream: - path: ../../packages/powersync_attachments_stream + powersync_attachments_stream: ^0.0.1 powersync: ^1.15.0 path_provider: ^2.1.1 supabase_flutter: ^2.0.1 diff --git a/packages/powersync_attachments_stream/.metadata b/packages/powersync_attachments_stream/.metadata deleted file mode 100644 index 8c47ce66..00000000 --- a/packages/powersync_attachments_stream/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: "d7b523b356d15fb81e7d340bbe52b47f93937323" - channel: "stable" - -project_type: package diff --git a/packages/powersync_attachments_stream/lib/common.dart b/packages/powersync_attachments_stream/lib/common.dart index e83f6928..42296b5d 100644 --- a/packages/powersync_attachments_stream/lib/common.dart +++ b/packages/powersync_attachments_stream/lib/common.dart @@ -4,5 +4,5 @@ export 'src/abstractions/sync_error_handler.dart'; export 'src/abstractions/attachment_service.dart'; export 'src/abstractions/attachment_context.dart'; export 'src/abstractions/local_storage.dart'; -export 'src/abstractions/remote_storage.dart'; +export 'src/abstractions/remote_storage.dart'; export 'src/attachment_queue_service.dart'; \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/powersync_attachments_stream.dart b/packages/powersync_attachments_stream/lib/powersync_attachments_stream.dart index 26356fc2..fba1e4cd 100644 --- a/packages/powersync_attachments_stream/lib/powersync_attachments_stream.dart +++ b/packages/powersync_attachments_stream/lib/powersync_attachments_stream.dart @@ -1,4 +1,3 @@ /// Default exports for native platforms (dart:io). For web, use 'common.dart'. export 'common.dart'; export 'src/storage/io_local_storage.dart'; -export 'src/attachment_queue_service.dart'; \ No newline at end of file diff --git a/packages/powersync_attachments_stream/lib/src/abstractions/attachment_context.dart b/packages/powersync_attachments_stream/lib/src/abstractions/attachment_context.dart index 268ed0b8..34fc8045 100644 --- a/packages/powersync_attachments_stream/lib/src/abstractions/attachment_context.dart +++ b/packages/powersync_attachments_stream/lib/src/abstractions/attachment_context.dart @@ -1,3 +1,5 @@ +import 'package:sqlite_async/sqlite_async.dart'; + import '../attachment.dart'; /// Context for performing Attachment operations. @@ -8,8 +10,7 @@ abstract class AbstractAttachmentContext { /// Delete the attachment from the attachment queue. /// /// [id]: The ID of the attachment to delete. - /// [tx]: The database context to use for the operation. - Future deleteAttachment(String id, dynamic context); + Future deleteAttachment(String id); /// Set the state of the attachment to ignore. Future ignoreAttachment(String id); @@ -38,12 +39,14 @@ abstract class AbstractAttachmentContext { /// Delete attachments which have been archived. /// /// Returns true if all items have been deleted. Returns false if there might be more archived items remaining. - Future deleteArchivedAttachments(Future Function(List) callback); + Future deleteArchivedAttachments( + Future Function(List) callback); /// Upserts an attachment record given a database connection context. /// /// [attachment]: The attachment to upsert. /// [context]: The database transaction/context to use for the operation. /// Returns the upserted [Attachment]. - Future upsertAttachment(Attachment attachment, dynamic context); -} \ No newline at end of file + Future upsertAttachment( + Attachment attachment, SqliteWriteContext context); +} diff --git a/packages/powersync_attachments_stream/lib/src/abstractions/attachment_service.dart b/packages/powersync_attachments_stream/lib/src/abstractions/attachment_service.dart index 3873a648..03409576 100644 --- a/packages/powersync_attachments_stream/lib/src/abstractions/attachment_service.dart +++ b/packages/powersync_attachments_stream/lib/src/abstractions/attachment_service.dart @@ -8,5 +8,6 @@ abstract class AbstractAttachmentService { /// Executes a callback with an exclusive lock on all attachment operations. /// This helps prevent race conditions between different updates. - Future withContext(Future Function(AbstractAttachmentContext context) action); -} \ No newline at end of file + Future withContext( + Future Function(AbstractAttachmentContext context) action); +} diff --git a/packages/powersync_attachments_stream/lib/src/abstractions/local_storage.dart b/packages/powersync_attachments_stream/lib/src/abstractions/local_storage.dart index a978d51f..5aac0dab 100644 --- a/packages/powersync_attachments_stream/lib/src/abstractions/local_storage.dart +++ b/packages/powersync_attachments_stream/lib/src/abstractions/local_storage.dart @@ -1,6 +1,6 @@ import 'dart:typed_data'; -abstract class AbstractLocalStorageAdapter { +abstract interface class LocalStorageAdapter { /// Saves binary data stream to storage at the specified file path /// /// [filePath] - Path where the file will be stored diff --git a/packages/powersync_attachments_stream/lib/src/abstractions/remote_storage.dart b/packages/powersync_attachments_stream/lib/src/abstractions/remote_storage.dart index d07122a8..0e98d25b 100644 --- a/packages/powersync_attachments_stream/lib/src/abstractions/remote_storage.dart +++ b/packages/powersync_attachments_stream/lib/src/abstractions/remote_storage.dart @@ -1,5 +1,5 @@ import 'dart:async'; - + import '../attachment.dart'; /// Adapter for interfacing with remote attachment storage. diff --git a/packages/powersync_attachments_stream/lib/src/abstractions/sync_error_handler.dart b/packages/powersync_attachments_stream/lib/src/abstractions/sync_error_handler.dart index cc2e3cab..7d55f9dc 100644 --- a/packages/powersync_attachments_stream/lib/src/abstractions/sync_error_handler.dart +++ b/packages/powersync_attachments_stream/lib/src/abstractions/sync_error_handler.dart @@ -3,7 +3,7 @@ import '../attachment.dart'; /// Interface for handling errors during attachment operations. /// Implementations determine whether failed operations should be retried. /// Attachment records are archived if an operation fails and should not be retried. -abstract class AbstractSyncErrorHandler { +abstract class SyncErrorHandler { /// Determines whether the provided attachment download operation should be retried. /// /// [attachment] The attachment involved in the failed download operation. @@ -33,4 +33,4 @@ abstract class AbstractSyncErrorHandler { Attachment attachment, Object exception, ); -} \ No newline at end of file +} diff --git a/packages/powersync_attachments_stream/lib/src/attachment.dart b/packages/powersync_attachments_stream/lib/src/attachment.dart index ec5ec0d5..65ddeb02 100644 --- a/packages/powersync_attachments_stream/lib/src/attachment.dart +++ b/packages/powersync_attachments_stream/lib/src/attachment.dart @@ -57,20 +57,28 @@ const defaultAttachmentsQueueTableName = AttachmentQueue.defaultTableName; class Attachment { /// Unique identifier for the attachment. final String id; + /// Timestamp of the last record update. final int timestamp; + /// Name of the attachment file, e.g., `[id].jpg`. final String filename; + /// Current state of the attachment, represented as an ordinal of [AttachmentState]. final AttachmentState state; + /// Local URI pointing to the attachment file, if available. final String? localUri; + /// Media type of the attachment, typically represented as a MIME type. final String? mediaType; + /// Size of the attachment in bytes, if available. final int? size; + /// Indicates whether the attachment has been synced locally before. final bool hasSynced; + /// Additional metadata associated with the attachment. final String? metaData; @@ -139,19 +147,19 @@ class AttachmentsQueueTable extends Table { List indexes = const [], String? viewName, }) : super.localOnly( - attachmentsQueueTableName, - [ - const Column.text('filename'), - const Column.text('local_uri'), - const Column.integer('timestamp'), - const Column.integer('size'), - const Column.text('media_type'), - const Column.integer('state'), - const Column.integer('has_synced'), - const Column.text('meta_data'), - ...additionalColumns, - ], - viewName: viewName, - indexes: indexes, - ); + attachmentsQueueTableName, + [ + const Column.text('filename'), + const Column.text('local_uri'), + const Column.integer('timestamp'), + const Column.integer('size'), + const Column.text('media_type'), + const Column.integer('state'), + const Column.integer('has_synced'), + const Column.text('meta_data'), + ...additionalColumns, + ], + viewName: viewName, + indexes: indexes, + ); } diff --git a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart index 102a5a11..1b66ed4d 100644 --- a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart +++ b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart @@ -9,8 +9,8 @@ import 'dart:async'; import 'package:logging/logging.dart'; import 'package:powersync_core/powersync_core.dart'; import 'package:sqlite_async/mutex.dart'; +import 'package:sqlite_async/sqlite_async.dart'; - import 'attachment.dart'; import 'abstractions/attachment_service.dart'; import 'abstractions/attachment_context.dart'; @@ -50,9 +50,9 @@ class WatchedAttachmentItem { this.filename, this.metaData, }) : assert( - fileExtension != null || filename != null, - 'Either fileExtension or filename must be provided.', - ); + fileExtension != null || filename != null, + 'Either fileExtension or filename must be provided.', + ); } /// Class used to implement the attachment queue. @@ -76,9 +76,9 @@ class AttachmentQueue { final PowerSyncDatabase db; final AbstractRemoteStorageAdapter remoteStorage; final Stream> Function() watchAttachments; - final AbstractLocalStorageAdapter localStorage; + final LocalStorageAdapter localStorage; final String attachmentsQueueTableName; - final AbstractSyncErrorHandler? errorHandler; + final SyncErrorHandler? errorHandler; final Duration syncInterval; final int archivedCacheLimit; final Duration syncThrottleDuration; @@ -188,9 +188,7 @@ class AttachmentQueue { String attachmentId, String? fileExtension, ) async { - return fileExtension != null - ? '$attachmentId.$fileExtension' - : '$attachmentId.dat'; + return '$attachmentId.${fileExtension ?? 'dat'}'; } /// Processes attachment items returned from [watchAttachments]. @@ -204,20 +202,15 @@ class AttachmentQueue { final List attachmentUpdates = []; for (final item in items) { - final existingQueueItem = currentAttachments - .cast() - .firstWhere( - (a) => a != null && a.id == item.id, - orElse: () => null, - ); + final existingQueueItem = + currentAttachments.where((a) => a.id == item.id).firstOrNull; if (existingQueueItem == null) { if (!downloadAttachments) continue; // This item should be added to the queue. // This item is assumed to be coming from an upstream sync. - final String filename = - item.filename ?? + final String filename = item.filename ?? await resolveNewAttachmentFilename(item.id, item.fileExtension); attachmentUpdates.add( @@ -286,8 +279,9 @@ class AttachmentQueue { required String mediaType, String? fileExtension, String? metaData, - required Future Function(dynamic context, Attachment attachment) - updateHook, + required Future Function( + SqliteWriteContext context, Attachment attachment) + updateHook, }) async { final row = await db.get('SELECT uuid() as id'); final id = row['id'] as String; @@ -323,8 +317,9 @@ class AttachmentQueue { /// The default implementation assumes the attachment record already exists locally. Future deleteFile({ required String attachmentId, - required Future Function(dynamic context, Attachment attachment) - updateHook, + required Future Function( + SqliteWriteContext context, Attachment attachment) + updateHook, }) async { return await attachmentsService.withContext((attachmentContext) async { final attachment = await attachmentContext.getAttachment(attachmentId); diff --git a/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart b/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart index 1fd6edff..bd3af39a 100644 --- a/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart +++ b/packages/powersync_attachments_stream/lib/src/implementations/attachment_context.dart @@ -1,6 +1,7 @@ import 'package:powersync_core/powersync_core.dart'; import 'package:powersync_core/sqlite3_common.dart'; import 'package:logging/logging.dart'; +import 'package:sqlite_async/sqlite_async.dart'; import '../abstractions/attachment_context.dart'; import '../attachment.dart'; @@ -24,9 +25,11 @@ class AttachmentContextImpl implements AbstractAttachmentContext { } @override - Future deleteAttachment(String id, dynamic context) async { + Future deleteAttachment(String id) async { log.info('deleteAttachment: $id'); - await context.execute('DELETE FROM $table WHERE id = ?', [id]); + await db.writeTransaction((tx) async { + await tx.execute('DELETE FROM $table WHERE id = ?', [id]); + }); } @override @@ -112,13 +115,15 @@ class AttachmentContextImpl implements AbstractAttachmentContext { maxArchivedCount, ], ); - final archivedAttachments = results.map((row) => Attachment.fromRow(row)).toList(); + final archivedAttachments = + results.map((row) => Attachment.fromRow(row)).toList(); if (archivedAttachments.isEmpty) { return false; } - log.info('Deleting ${archivedAttachments.length} archived attachments (exceeding maxArchivedCount=$maxArchivedCount)...'); + log.info( + 'Deleting ${archivedAttachments.length} archived attachments (exceeding maxArchivedCount=$maxArchivedCount)...'); // Call the callback with the list of archived attachments before deletion await callback(archivedAttachments); @@ -137,9 +142,8 @@ class AttachmentContextImpl implements AbstractAttachmentContext { @override Future upsertAttachment( Attachment attachment, - dynamic context, + SqliteWriteContext context, ) async { - await context.execute( '''INSERT OR REPLACE INTO $table (id, timestamp, filename, local_uri, media_type, size, state, has_synced, meta_data) diff --git a/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart b/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart index 8655641b..864c594a 100644 --- a/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart +++ b/packages/powersync_attachments_stream/lib/src/storage/io_local_storage.dart @@ -6,10 +6,10 @@ import 'package:path/path.dart' as p; import '../abstractions/local_storage.dart'; -/// Implements [AbstractLocalStorageAdapter] for device filesystem using Dart IO. +/// Implements [LocalStorageAdapter] for device filesystem using Dart IO. /// /// Handles file and directory operations for attachments. -class IOLocalStorage implements AbstractLocalStorageAdapter { +class IOLocalStorage implements LocalStorageAdapter { final String attachmentsDirectory; late final Directory baseDir; diff --git a/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart b/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart index 362e0f53..49cf5a3c 100644 --- a/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart +++ b/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart @@ -33,9 +33,9 @@ import '../abstractions/sync_error_handler.dart'; /// - [errorHandler]: Optional error handler for managing sync-related errors. class SyncingService { final AbstractRemoteStorageAdapter remoteStorage; - final AbstractLocalStorageAdapter localStorage; + final LocalStorageAdapter localStorage; final AbstractAttachmentService attachmentsService; - final AbstractSyncErrorHandler? errorHandler; + final SyncErrorHandler? errorHandler; final Duration syncThrottle; final Duration period; final Logger logger; @@ -75,27 +75,27 @@ class SyncingService { // Merge both streams and apply throttling final mergedStream = StreamGroup.merge([attachmentChanges, manualTriggers]).listen(( - _, - ) async { - try { - await attachmentsService.withContext((context) async { - final attachments = await context.getActiveAttachments(); - logger.info('Found ${attachments.length} active attachments'); - await handleSync(attachments, context); - await deleteArchivedAttachments(context); - }); - } catch (e, st) { - if (e is! StateError && e.toString().contains('cancelled')) { - logger.severe( - 'Caught exception when processing attachments', - e, - st, - ); - } else { - rethrow; - } - } + _, + ) async { + try { + await attachmentsService.withContext((context) async { + final attachments = await context.getActiveAttachments(); + logger.info('Found ${attachments.length} active attachments'); + await handleSync(attachments, context); + await deleteArchivedAttachments(context); }); + } catch (e, st) { + if (e is! StateError && e.toString().contains('cancelled')) { + logger.severe( + 'Caught exception when processing attachments', + e, + st, + ); + } else { + rethrow; + } + } + }); _syncSubscription = mergedStream; @@ -155,7 +155,7 @@ class SyncingService { break; case AttachmentState.queuedDelete: logger.info('Deleting [${attachment.filename}]'); - updatedAttachments.add(await deleteAttachment(attachment)); + updatedAttachments.add(await deleteAttachment(attachment, context)); break; case AttachmentState.synced: logger.info('Attachment ${attachment.id} is already synced'); @@ -257,7 +257,7 @@ class SyncingService { /// /// [attachment]: The attachment to delete. /// Returns the updated attachment with its new state. - Future deleteAttachment(Attachment attachment) async { + Future deleteAttachment(Attachment attachment, AbstractAttachmentContext context) async { try { logger.info('Deleting attachment ${attachment.id} from remote storage'); await remoteStorage.deleteFile(attachment); @@ -266,6 +266,8 @@ class SyncingService { await localStorage.fileExists(attachment.localUri!)) { await localStorage.deleteFile(attachment.localUri!); } + // Remove the attachment record from the queue in a transaction. + await context.deleteAttachment(attachment.id); return attachment.copyWith(state: AttachmentState.archived); } catch (e, st) { if (errorHandler != null) { diff --git a/packages/powersync_attachments_stream/pubspec.lock b/packages/powersync_attachments_stream/pubspec.lock index c10c760c..3ccccd46 100644 --- a/packages/powersync_attachments_stream/pubspec.lock +++ b/packages/powersync_attachments_stream/pubspec.lock @@ -488,13 +488,13 @@ packages: source: hosted version: "7.0.0" sqlite3: - dependency: transitive + dependency: "direct main" description: name: sqlite3 - sha256: "608b56d594e4c8498c972c8f1507209f9fd74939971b948ddbbfbfd1c9cb3c15" + sha256: f393d92c71bdcc118d6203d07c991b9be0f84b1a6f89dd4f7eed348131329924 url: "https://pub.dev" source: hosted - version: "2.7.7" + version: "2.9.0" sqlite3_web: dependency: transitive description: @@ -672,5 +672,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.8.1 <4.0.0" + dart: ">=3.8.0 <4.0.0" flutter: ">=3.27.0" diff --git a/packages/powersync_attachments_stream/pubspec.yaml b/packages/powersync_attachments_stream/pubspec.yaml index cdb51763..6b830120 100644 --- a/packages/powersync_attachments_stream/pubspec.yaml +++ b/packages/powersync_attachments_stream/pubspec.yaml @@ -5,7 +5,7 @@ repository: https://github.com/powersync-ja/powersync.dart homepage: https://www.powersync.com/ environment: - sdk: ^3.8.1 + sdk: ^3.4.0 dependencies: flutter: @@ -16,6 +16,7 @@ dependencies: logging: ^1.2.0 sqlite_async: ^0.11.0 path_provider: ^2.0.13 + sqlite3: ^2.9.0 dev_dependencies: flutter_test: diff --git a/packages/powersync_attachments_stream/test/local_storage_test.dart b/packages/powersync_attachments_stream/test/local_storage_test.dart index 73a2a7ec..70965973 100644 --- a/packages/powersync_attachments_stream/test/local_storage_test.dart +++ b/packages/powersync_attachments_stream/test/local_storage_test.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; - + import 'package:test/test.dart'; import 'package:path/path.dart' as p; import 'package:powersync_attachments_stream/src/storage/io_local_storage.dart'; @@ -107,7 +107,8 @@ void main() { }); group('edge cases and robustness', () { - test('saveFile with empty data writes empty file and returns 0 size', () async { + test('saveFile with empty data writes empty file and returns 0 size', + () async { const filePath = 'empty_file'; final emptyBytes = Uint8List(0); @@ -130,7 +131,8 @@ void main() { Uint8List.fromList([3, 4]), Uint8List.fromList([5, 6, 7, 8]), ]; - final expectedBytes = Uint8List.fromList(chunks.expand((c) => c).toList()); + final expectedBytes = + Uint8List.fromList(chunks.expand((c) => c).toList()); await storage.saveFile(filePath, expectedBytes); final outChunks = await storage.readFile(filePath).toList(); @@ -161,7 +163,8 @@ void main() { expect(await storage.fileExists(filePath), isTrue); }); - test('clear works even if base directory was removed externally', () async { + test('clear works even if base directory was removed externally', + () async { await storage.initialize(); // Remove the base dir manually @@ -186,12 +189,14 @@ void main() { await d.file(filePath, bytes).validate(); }); - test('readFile accepts mediaType parameter (ignored by IO impl)', () async { + test('readFile accepts mediaType parameter (ignored by IO impl)', + () async { const filePath = 'with_media_type'; final data = Uint8List.fromList([1, 2, 3]); await storage.saveFile(filePath, data); - final result = await storage.readFile(filePath, mediaType: 'image/jpeg').toList(); + final result = + await storage.readFile(filePath, mediaType: 'image/jpeg').toList(); expect(result, equals([data])); }); }); @@ -354,4 +359,4 @@ void main() { }); }); }); -} \ No newline at end of file +} From 10ded4e5f5951a7e74f33eaa5d1d135f38f6bee6 Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Thu, 14 Aug 2025 12:05:17 +0200 Subject: [PATCH 18/18] Formatting fixes --- .../lib/attachments/photo_capture_widget.dart | 11 ++++++----- demos/supabase-todolist/lib/attachments/queue.dart | 3 +-- .../lib/widgets/todo_item_widget.dart | 4 ++-- demos/supabase-todolist/test/widget_test.dart | 4 +++- .../powersync_attachments_stream/lib/common.dart | 2 +- .../lib/src/attachment_queue_service.dart | 4 +--- .../lib/src/sync/syncing_service.dart | 13 +++++-------- 7 files changed, 19 insertions(+), 22 deletions(-) diff --git a/demos/supabase-todolist/lib/attachments/photo_capture_widget.dart b/demos/supabase-todolist/lib/attachments/photo_capture_widget.dart index 86fa7d76..b41caa90 100644 --- a/demos/supabase-todolist/lib/attachments/photo_capture_widget.dart +++ b/demos/supabase-todolist/lib/attachments/photo_capture_widget.dart @@ -9,7 +9,8 @@ class TakePhotoWidget extends StatefulWidget { final String todoId; final CameraDescription camera; - const TakePhotoWidget({super.key, required this.todoId, required this.camera}); + const TakePhotoWidget( + {super.key, required this.todoId, required this.camera}); @override State createState() { @@ -45,19 +46,19 @@ class _TakePhotoWidgetState extends State { log.info('Taking photo for todo: ${widget.todoId}'); await _initializeControllerFuture; final XFile photo = await _cameraController.takePicture(); - + // Read the photo data as bytes final photoFile = File(photo.path); if (!await photoFile.exists()) { log.warning('Photo file does not exist: ${photo.path}'); return; } - + final photoData = await photoFile.readAsBytes(); - + // Save the photo attachment with the byte data final attachment = await savePhotoAttachment(photoData, widget.todoId); - + log.info('Photo attachment saved with ID: ${attachment.id}'); } catch (e) { log.severe('Error taking photo: $e'); diff --git a/demos/supabase-todolist/lib/attachments/queue.dart b/demos/supabase-todolist/lib/attachments/queue.dart index 4a5375b1..8ec6c918 100644 --- a/demos/supabase-todolist/lib/attachments/queue.dart +++ b/demos/supabase-todolist/lib/attachments/queue.dart @@ -33,8 +33,7 @@ Future initializeAttachmentQueue(PowerSyncDatabase db) async { await attachmentQueue.startSync(); } -Future savePhotoAttachment( - List photoData, String todoId, +Future savePhotoAttachment(List photoData, String todoId, {String mediaType = 'image/jpeg'}) async { // Save the file using the AttachmentQueue API return await attachmentQueue.saveFile( diff --git a/demos/supabase-todolist/lib/widgets/todo_item_widget.dart b/demos/supabase-todolist/lib/widgets/todo_item_widget.dart index ce5b3575..700a869a 100644 --- a/demos/supabase-todolist/lib/widgets/todo_item_widget.dart +++ b/demos/supabase-todolist/lib/widgets/todo_item_widget.dart @@ -23,11 +23,11 @@ class TodoItemWidget extends StatelessWidget { Future deleteTodo(TodoItem todo) async { if (todo.photoId != null) { - await attachmentQueue.deleteFile( attachmentId: todo.photoId!, updateHook: (context, attachment) async { - await context.execute("UPDATE todos SET photo_id = NULL WHERE id = ?", [todo.id]); + await context.execute( + "UPDATE todos SET photo_id = NULL WHERE id = ?", [todo.id]); }, ); } diff --git a/demos/supabase-todolist/test/widget_test.dart b/demos/supabase-todolist/test/widget_test.dart index 77913e2e..33dea10a 100644 --- a/demos/supabase-todolist/test/widget_test.dart +++ b/demos/supabase-todolist/test/widget_test.dart @@ -13,7 +13,9 @@ import 'package:powersync_flutter_demo/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp(loggedIn: true,)); + await tester.pumpWidget(const MyApp( + loggedIn: true, + )); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); diff --git a/packages/powersync_attachments_stream/lib/common.dart b/packages/powersync_attachments_stream/lib/common.dart index 42296b5d..36c8af1f 100644 --- a/packages/powersync_attachments_stream/lib/common.dart +++ b/packages/powersync_attachments_stream/lib/common.dart @@ -5,4 +5,4 @@ export 'src/abstractions/attachment_service.dart'; export 'src/abstractions/attachment_context.dart'; export 'src/abstractions/local_storage.dart'; export 'src/abstractions/remote_storage.dart'; -export 'src/attachment_queue_service.dart'; \ No newline at end of file +export 'src/attachment_queue_service.dart'; diff --git a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart index 1b66ed4d..8d967958 100644 --- a/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart +++ b/packages/powersync_attachments_stream/lib/src/attachment_queue_service.dart @@ -8,7 +8,6 @@ import 'dart:async'; import 'package:logging/logging.dart'; import 'package:powersync_core/powersync_core.dart'; -import 'package:sqlite_async/mutex.dart'; import 'package:sqlite_async/sqlite_async.dart'; import 'attachment.dart'; @@ -108,11 +107,10 @@ class AttachmentQueue { }) : logger = logger ?? Logger('AttachmentQueue') { attachmentsService = AttachmentServiceImpl( db: db, - logger: logger ?? Logger('AttachmentQueue'), + logger: this.logger, maxArchivedCount: archivedCacheLimit, attachmentsQueueTableName: attachmentsQueueTableName, ); - syncingService = SyncingService( remoteStorage: remoteStorage, localStorage: localStorage, diff --git a/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart b/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart index 49cf5a3c..8bd50f3a 100644 --- a/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart +++ b/packages/powersync_attachments_stream/lib/src/sync/syncing_service.dart @@ -56,11 +56,7 @@ class SyncingService { }) : logger = logger ?? Logger('SyncingService'); /// Starts the syncing process, including periodic and event-driven sync operations. - /// - /// [period] is the interval at which periodic sync operations are triggered. - Future startSync({ - Duration period = const Duration(seconds: 30), - }) async { + Future startSync() async { if (_isClosed) return; _syncSubscription?.cancel(); @@ -99,8 +95,8 @@ class SyncingService { _syncSubscription = mergedStream; - // Start periodic sync - _periodicSubscription = Stream.periodic(period, (_) => null).listen(( + // Start periodic sync using instance period + _periodicSubscription = Stream.periodic(period, (_) {}).listen(( _, ) { logger.info('Periodically syncing attachments'); @@ -257,7 +253,8 @@ class SyncingService { /// /// [attachment]: The attachment to delete. /// Returns the updated attachment with its new state. - Future deleteAttachment(Attachment attachment, AbstractAttachmentContext context) async { + Future deleteAttachment( + Attachment attachment, AbstractAttachmentContext context) async { try { logger.info('Deleting attachment ${attachment.id} from remote storage'); await remoteStorage.deleteFile(attachment);