diff --git a/demos/supabase-trello/.env.template b/demos/supabase-trello/.env.template new file mode 100644 index 00000000..073dd3bb --- /dev/null +++ b/demos/supabase-trello/.env.template @@ -0,0 +1,4 @@ +# Update these with your own values +SUPABASE_URL=https://aaaaaaaaaaaa.supabase.co +SUPABASE_ANON_KEY=my_anon_key +POWERSYNC_URL=https://aaaaaaaaaaaa.powersync.journeyapps.com diff --git a/demos/supabase-trello/.gitignore b/demos/supabase-trello/.gitignore new file mode 100644 index 00000000..7f4b32b9 --- /dev/null +++ b/demos/supabase-trello/.gitignore @@ -0,0 +1,47 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +.env + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/demos/supabase-trello/.metadata b/demos/supabase-trello/.metadata new file mode 100644 index 00000000..2e239dba --- /dev/null +++ b/demos/supabase-trello/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: android + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: ios + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: linux + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: macos + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: web + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: windows + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/demos/supabase-trello/LICENSE b/demos/supabase-trello/LICENSE new file mode 100644 index 00000000..0e259d42 --- /dev/null +++ b/demos/supabase-trello/LICENSE @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/demos/supabase-trello/README.md b/demos/supabase-trello/README.md new file mode 100644 index 00000000..a267c88e --- /dev/null +++ b/demos/supabase-trello/README.md @@ -0,0 +1,342 @@ +# Trello Clone Using PowerSync + Flutter + +## Introduction + +Trello clone app built with [Flutter](https://flutter.dev/), [PowerSync](https://powersync.co/) and [Supabase](https://supabase.io/) + +drawing + +# Running the app + +Ensure you have [melos](https://melos.invertase.dev/~melos-latest/getting-started) installed. + +1. `cd demos/supabase-trello` +2. `melos prepare` +3. `cp .env.template .env` +4. Insert your Supabase and PowerSync project credentials into `.env` (See instructions below) +5. `flutter run` + +# Getting Started + +First check out [the integration guide](https://docs.powersync.co/integration-guides/supabase-+-powersync) for [PowerSync](https://powersync.co/) and [Supabase](https://supabase.io/). + +Before you proceed, we assume that you have already signed up for free accounts with both Supabase and PowerSync. If you haven't signed up for a **PowerSync account** yet, [click here](https://accounts.journeyapps.com/portal/free-trial?powersync=true) (and if you haven't signed up for **Supabase** yet, [click here](https://supabase.com/dashboard/sign-up)). We also assume that you already have Flutter set up. + +Next up we will follow these steps: + +1. Configure Supabase: + - Create the database schema + - Create the Postgres publication +2. Configure PowerSync: + - Create connection to Supabase + - Configure global Sync Rules +4. Configure the Trello clone app +5. Run the app and test +6. Configure improved Sync Rules +7. Run the app and test + +## Configure Supabase + +- Create a new Supabase project (or use an existing project if you prefer) and follow the below steps. +- For ease of use of this Flutter app, you can **disable email confirmation** in your Supabase Auth settings. In your Supabase project, go to _"Authentication"_ --> _"Providers"_ -> _"Email"_ and then disable _"Confirm email"_. If you keep email confirmation enabled, the Supabase user confirmation email will reference the default Supabase Site URL of http://localhost:3000 — you can ignore this. + +### Create the Database Schema +After creating the Supabase project, we still need to create the tables in the database. For this application we need the following tables: + +* `activity` +* `attachment` +* `board` +* `card` +* `checklist` +* `comment` +* `listboard` +* `member` +* `models` +* `trellouser` +* `workspace` +* `board_label` +* `card_label` + +_(We give a brief overview of the app domain model later in this README.)_ + +Do the following: +- Open the `tables.sql` file, and copy the contents. +- Paste this into the *Supabase SQL Editor* +- Run the SQL statements in the Supabase SQL Editor. (If you get a warning about a "potentially destructive operation", that's a false positive that you can safely ignore.) + +### Create the Postgres Publication + +PowerSync uses the Postgres [Write Ahead Log (WAL)](https://www.postgresql.org/docs/current/wal-intro.html) to replicate data changes in order to keep PowerSync SDK clients up to date. To enable this we need to create a `publication` in Supabase. + +Run the below SQL statement in your Supabase SQL Editor: +```sql +create publication powersync for table activity, attachment, board, card, checklist, comment, listboard, member, trellouser, workspace, board_label, card_label; +``` + +## Configuring PowerSync + +We need to connect PowerSync to the Supabase Postgres database: + +- In the [PowerSync dashboard](https://powersync.journeyapps.com/) project tree, click on _"Create new instance"_. + +- Give your instance a name, such as _"Trello Clone"_. + +- In the _"Edit Instance"_ dialog, navigate to the _"Credentials"_ tab and enable _"Use Supabase Auth"_. + +- Under the _"Connections"_ tab, click on the + icon. + +- On the subsequent screen, we'll configure the connection to Supabase. This is simplest using your Supabase database URI. In your Supabase dashboard, navigate to _"Project Settings"_ -> _"Database"_. Then, under the _"Connection String"_ section, switch to URI and copy the value. + +- Paste the copied value into the _"URI"_ field in PowerSync. + +- Enter the Password for the `postgres` user in your Supabase database. (Supabase also [refers to this password](https://supabase.com/docs/guides/database/managing-passwords) as the database password or project password) + +- Click _"Test Connection"_ and fix any errors. + +- Click "Save" + +PowerSync deploys and configures an isolated cloud environment for you, which will take a few minutes to complete. + + +## Configuring Sync Rules - 1 + +PowerSync [Sync Rules](https://docs.powersync.co/usage/sync-rules) allow developers to control which data gets synced to which user devices using a SQL-like syntax in a YAML file. For this Trello clone demo app, we're first going to use naive global sync rules, and then present improved rules that take the domain permissions into account. + + +### Global Sync Rules to Get Things Working + +We can be naive about it, and start by using a global bucket definition that at least specifies in some way which users can get data. + +- Copy the contents of `sync-rules-0.yaml` to `sync-rules.yaml` under your PowerSync project instance. +- In the top right of the `sync-rules.yaml` editor, click _"Deploy sync rules"_. +- Confirm in the dialog and wait a couple of minutes for the deployment to complete. + +When you now run the app (after completing the next step to configure and run the app), it will actually show and retain data. The app code itself applies some basic filtering to only show data that belongs to the current user, or according to the visibility and membership settings of the various workspaces and boards. + + +## Configuring Flutter App + +We need to configure the app to use the correct PowerSync and Supabase projects. + +- Copy the `trelloappclone_flutter/.env.template` file to `trelloappclone_flutter/.env`. +- Replace the values for `SUPABASE_URL` and `SUPABASE_ANON_KEY` (You can find these under _"Project Settings"_ -> _"API"_ in your Supabase dashboard — under the _"URL"_ section, and anon key under _"Project API keys"_.) +- For the value of `POWERSYNC_URL`, follow these steps: + 1. In the project tree on the PowerSync dashboard, right-click on the instance you created earlier. + 2. Click _"Edit instance"_. + 3. Click on _"Instance URL"_ to copy the value. + + +## Build & Run the Flutter App + +- Run ``` flutter pub get ``` to install the necessary packages (in the root directory of the project.) +- Invoke the ``` flutter run ``` command, and select either an Android device/emulator or iOS device/simulator as destination (_Note: PowerSync does not support Flutter web apps yet._) + + +## Configuring Sync Rules - 2 + +### Using Sync Rules to Enforce Permissions +We have syncing working, but the sync rules are not enforcing the access rules from the domain in any way. + +It is better that we do not sync data to the client that the logged-in user is not allowed to see. We can use PowerSync sync rules to enforce permissions, so that users can only see and edit data that they are allowed to see and edit. + +First, we need to understand the permissions from the app domain model: + +- A **workspace** is created by a user — this user can always see and edit the workspace. +- A **workspace** has a specific *visibility*: private (only the owner can see it), workspace (only owner and members can see it), or public (anyone can see it). +- A **workspace** has a list of *members* (users) that can see and edit the workspace, if the workspace is not private. +- A **board** is created by a user — this user can always see and edit the board as long as the user can still access that workspace +- A **board** has a specific *visibility*: private (only the owner can see it), workspace (only owner and members belonging to the parent workspace can see it) +- A user can see (and edit) any of the **cards** and **lists** belonging to a **board** that they have access to. +- A user can see (and edit) any of the **checklists**, **comments**, and **attachments** belonging to a **card** that they have access to. + +Also have a look at `trelloappclone_flutter/lib/utils/service.dart` for the access patterns used by the app code. + +Let's explore how we can use PowerSync Sync Rules to enforce these permissions and access patterns. + +First we want to sync the relevant `trellouser` records synced to the local database. To enable lookups of users for adding as members to workspaces, we currently sync all user records. For a production app, we would ideally work via an API to invite members, and not worry about direct data lookups on the app side. + +```yaml +bucket_definitions: + user_info: + # this allows syncing of all trellouser records so we can lookup users when adding members + data: + - SELECT * FROM trellouser +``` + +Then we want to look up all the workspaces (a) owned by this user, (b) where this user is a member, or (c) which are public. + +```yaml + by_workspace: + # the entities are filtered by workspaceId, thus linked to the workspaces (a) owned by this user, (b) where this user is a member, or (c) which are public + # Note: the quotes for "workspaceId" and "userId" is important, since otherwise Postgres does not deal well with non-lowercase identifiers + parameters: + - SELECT id as workspace_id FROM workspace WHERE + workspace."userId" = token_parameters.user_id + - SELECT "workspaceId" as workspace_id FROM member WHERE + member."userId" = token_parameters.user_id + - SELECT id as workspace_id FROM workspace WHERE + visibility = "Public" + data: + - SELECT * FROM workspace WHERE workspace.id = bucket.workspace_id + - SELECT * FROM board WHERE board."workspaceId" = bucket.workspace_id + - SELECT * FROM member WHERE member."workspaceId" = bucket.workspace_id + - SELECT * FROM listboard WHERE listboard."workspaceId" = bucket.workspace_id + - SELECT * FROM card WHERE card."workspaceId" = bucket.workspace_id + - SELECT * FROM checklist WHERE checklist."workspaceId" = bucket.workspace_id + - SELECT * FROM activity WHERE activity."workspaceId" = bucket.workspace_id + - SELECT * FROM comment WHERE comment."workspaceId" = bucket.workspace_id + - SELECT * FROM attachment WHERE attachment."workspaceId" = bucket.workspace_id + - SELECT * FROM board_label WHERE board_label."workspaceId" = bucket.workspace_id + - SELECT * FROM card_label WHERE card_label."workspaceId" = bucket.workspace_id +``` + +**To Configure the Improved Sync Rules, Follow These Steps:** + +- Copy the contents of `sync-rules-1.yaml`. +- Paste this to `sync-rules.yaml` under your PowerSync project instance +- Click _"Deploy sync rules"_. +- Confirm in the dialog and wait a couple of minutes for the deployment to complete. + +Now you can run the app again, and it should now only sync the subset of data that a logged in user actually has access to. + +### Importing / Generating Data + +When you run the app, after logging in, you will start without any workspaces or boards. It is possible to generate a workspace with sample boards and cards in order to make it easier to have enough data to experiment with, without having to manually create every item. + +- Sign up and log in to the app. +- In the home view, tap on the "+" floating button in the lower right corner. +- Tap on _"Sample Workspace"_ and give it a name — this will create a new workspace, with multiple boards with lists, and a random number of cards with checklists, comments and activities for each list. + +drawing + +### Structure + +* The data models are in `lib/models`) +* A PowerSync client that makes use of the PowerSync Flutter SDK and adds a few convenience methods for the app use cases (`lib/protocol/powersync.dart`) +* A `DataClient` API that can be used from the app code, and provides the higher level data API. (`lib/protocol/data_client.dart`) + +Two important files to point out as well are: + +* `lib/schema.dart` defines the SQLite schema to use for the local synced datastore, and this maps to the model classes. +* `lib/app_config.dart` contains the tokens and URLs needed to connect to PowerSync and Supabase. + + +### Listening to Updated Data + +The PowerSync SDK makes use of `watch` queries to listen for changes to synced data. When the SDK syncs updated data from the server, and it matches the query, it will send an event out, allowing e.g. an app view (via `StreamBuilder`) or some other state management class to respond. + +As an example look at `trelloappclone_flutter/lib/utils/service.dart`: + +```dart + Stream> getWorkspacesStream() { + return dataClient.workspace.watchWorkspacesByUser(userId: trello.user.id).map((workspaces) { + trello.setWorkspaces(workspaces); + return workspaces; + }); + } +``` + +This in turn makes use of the `_WorkspaceRepository` class: +```dart + Stream> watchWorkspacesByUser({required String userId}) { + //First we get the workspaces + return client.getDBExecutor().watch(''' + SELECT * FROM workspace WHERE userId = ? + ''', parameters: [userId]).asyncMap((event) async { + List workspaces = event.map((row) => Workspace.fromRow(row)).toList(); + + //Then we get the members for each workspace + for (Workspace workspace in workspaces) { + List members = await client.member.getMembersByWorkspace(workspaceId: workspace.id); + workspace.members = members; + } + return workspaces; + }); + } +``` + +Now we can simply make use of a `StreamBuilder` to have a view component that updates whenever a matching workspace is updated: + +```dart + StreamBuilder( + stream: getWorkspacesStream(), + builder: + (BuildContext context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + List children = snapshot.data as List; + + if (children.isNotEmpty) { + return SingleChildScrollView( + child: + Column(children: buildWorkspacesAndBoards(children))); + } + } else { + return Container(); + } + }) +``` + +### Doing a Transaction + +It is possible to ensure that a number of related entities are updated in an atomic database transaction. As an example of this, look at `DataClient.archiveCardsInList`: + +```dart + /// Archive cards in and return how many were archived + /// This happens in a transaction + Future archiveCardsInList(Listboard list) async { + if (list.cards == null || list.cards!.isEmpty) { + return 0; + } + + //start a write transaction + return client.getDBExecutor().writeTransaction((sqlContext) async { + List cards = list.cards!; + int numCards = cards.length; + + //we set each of the cards in the list to archived = true + sqlContext.executeBatch(''' + UPDATE card + SET archived = 1 + WHERE id = ? + ''' + , cards.map((card) => [card.id]).toList()); + + //touch listboard to trigger update via stream listeners on Listboard + sqlContext.execute(''' + UPDATE listboard + SET archived = 0 + WHERE id = ? + ''', [list.id]); + + list.cards = []; + return numCards; + //end of transaction + }, debugContext: 'archiveCardsInList'); + } +``` + +The above code is invoked if you choose to archive all the cards in a list from the list popup menu. + +### Changes from Original Trello Clone App + +The app code was forked from the [Serverpod + Flutter Tutorial](https://github.com/Mobterest/serverpod_flutter_tutorial) code. It was changed in the following ways to facilitate the PowerSync integration: + +- Updated data model so that all `id` fields are Strings, and use UUIDs (it was auto-increment integer fields in the original app) +- Updated data model so that all entities refer to the `workspaceId` of workspace in which it was created (this facilitates the Sync Rules) + +## Next + +Below is a list of things that can be implemented to enhance the functionality and experience of this app: + +* Update Workspace + Board edit views to use actual data and update the entity +* Get Comments & Checklists working properly +* Enhance state management - e.g. let `TrelloProvider` listen to streams, and notify changes, to simplify views +* Get the attachments to actually work (using Supabase files upload/download) + +## Feedback + +- Feel free to send feedback. Feature requests are always welcome. If there's anything you'd like to chat about, please [join our Discord](https://discord.gg/powersync). + +## Acknowledgements + +This tutorial is based on the [Serverpod + Flutter Tutorial](https://github.com/Mobterest/serverpod_flutter_tutorial) by [Mobterest](https://www.instagram.com/mobterest/) diff --git a/demos/supabase-trello/analysis_options.yaml b/demos/supabase-trello/analysis_options.yaml new file mode 100644 index 00000000..61b6c4de --- /dev/null +++ b/demos/supabase-trello/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/demos/supabase-trello/android/.gitignore b/demos/supabase-trello/android/.gitignore new file mode 100644 index 00000000..6f568019 --- /dev/null +++ b/demos/supabase-trello/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/demos/supabase-trello/android/app/build.gradle b/demos/supabase-trello/android/app/build.gradle new file mode 100644 index 00000000..21dc0f37 --- /dev/null +++ b/demos/supabase-trello/android/app/build.gradle @@ -0,0 +1,65 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +android { + namespace "com.example.trelloappclone_flutter" + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.trelloappclone_flutter" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion 23 + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} diff --git a/demos/supabase-trello/android/app/src/debug/AndroidManifest.xml b/demos/supabase-trello/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/demos/supabase-trello/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/demos/supabase-trello/android/app/src/main/AndroidManifest.xml b/demos/supabase-trello/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..e1dc4071 --- /dev/null +++ b/demos/supabase-trello/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/demos/supabase-trello/android/app/src/main/kotlin/com/example/trelloappclone_flutter/MainActivity.kt b/demos/supabase-trello/android/app/src/main/kotlin/com/example/trelloappclone_flutter/MainActivity.kt new file mode 100644 index 00000000..875d532e --- /dev/null +++ b/demos/supabase-trello/android/app/src/main/kotlin/com/example/trelloappclone_flutter/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.trelloappclone_flutter + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/demos/supabase-trello/android/app/src/main/res/drawable-v21/launch_background.xml b/demos/supabase-trello/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/demos/supabase-trello/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/demos/supabase-trello/android/app/src/main/res/drawable/launch_background.xml b/demos/supabase-trello/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/demos/supabase-trello/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/demos/supabase-trello/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/supabase-trello/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..db77bb4b Binary files /dev/null and b/demos/supabase-trello/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/demos/supabase-trello/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/supabase-trello/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..17987b79 Binary files /dev/null and b/demos/supabase-trello/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/demos/supabase-trello/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/supabase-trello/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..09d43914 Binary files /dev/null and b/demos/supabase-trello/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/demos/supabase-trello/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/supabase-trello/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..d5f1c8d3 Binary files /dev/null and b/demos/supabase-trello/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/demos/supabase-trello/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/supabase-trello/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..4d6372ee Binary files /dev/null and b/demos/supabase-trello/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/demos/supabase-trello/android/app/src/main/res/values-night/styles.xml b/demos/supabase-trello/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..06952be7 --- /dev/null +++ b/demos/supabase-trello/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/demos/supabase-trello/android/app/src/main/res/values/styles.xml b/demos/supabase-trello/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..cb1ef880 --- /dev/null +++ b/demos/supabase-trello/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/demos/supabase-trello/android/app/src/profile/AndroidManifest.xml b/demos/supabase-trello/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/demos/supabase-trello/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/demos/supabase-trello/android/build.gradle b/demos/supabase-trello/android/build.gradle new file mode 100644 index 00000000..bc157bd1 --- /dev/null +++ b/demos/supabase-trello/android/build.gradle @@ -0,0 +1,18 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/demos/supabase-trello/android/gradle.properties b/demos/supabase-trello/android/gradle.properties new file mode 100644 index 00000000..94adc3a3 --- /dev/null +++ b/demos/supabase-trello/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/demos/supabase-trello/android/gradle/wrapper/gradle-wrapper.properties b/demos/supabase-trello/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..348c409e --- /dev/null +++ b/demos/supabase-trello/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip diff --git a/demos/supabase-trello/android/settings.gradle b/demos/supabase-trello/android/settings.gradle new file mode 100644 index 00000000..e9862544 --- /dev/null +++ b/demos/supabase-trello/android/settings.gradle @@ -0,0 +1,25 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.7.0" apply false + id "org.jetbrains.kotlin.android" version "1.7.0" apply false +} + +include ":app" diff --git a/demos/supabase-trello/assets/landing.jpg b/demos/supabase-trello/assets/landing.jpg new file mode 100644 index 00000000..7614ae9f Binary files /dev/null and b/demos/supabase-trello/assets/landing.jpg differ diff --git a/demos/supabase-trello/assets/trello-logo.png b/demos/supabase-trello/assets/trello-logo.png new file mode 100644 index 00000000..a8068fd6 Binary files /dev/null and b/demos/supabase-trello/assets/trello-logo.png differ diff --git a/demos/supabase-trello/devtools_options.yaml b/demos/supabase-trello/devtools_options.yaml new file mode 100644 index 00000000..fa0b357c --- /dev/null +++ b/demos/supabase-trello/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/demos/supabase-trello/ios/.gitignore b/demos/supabase-trello/ios/.gitignore new file mode 100644 index 00000000..7a7f9873 --- /dev/null +++ b/demos/supabase-trello/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/demos/supabase-trello/ios/Flutter/AppFrameworkInfo.plist b/demos/supabase-trello/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 00000000..7c569640 --- /dev/null +++ b/demos/supabase-trello/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/demos/supabase-trello/ios/Flutter/Debug.xcconfig b/demos/supabase-trello/ios/Flutter/Debug.xcconfig new file mode 100644 index 00000000..ec97fc6f --- /dev/null +++ b/demos/supabase-trello/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/demos/supabase-trello/ios/Flutter/Release.xcconfig b/demos/supabase-trello/ios/Flutter/Release.xcconfig new file mode 100644 index 00000000..c4855bfe --- /dev/null +++ b/demos/supabase-trello/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/demos/supabase-trello/ios/Podfile b/demos/supabase-trello/ios/Podfile new file mode 100644 index 00000000..885313b0 --- /dev/null +++ b/demos/supabase-trello/ios/Podfile @@ -0,0 +1,47 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '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__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' + end + end +end diff --git a/demos/supabase-trello/ios/Podfile.lock b/demos/supabase-trello/ios/Podfile.lock new file mode 100644 index 00000000..4ca14fbb --- /dev/null +++ b/demos/supabase-trello/ios/Podfile.lock @@ -0,0 +1,136 @@ +PODS: + - app_links (0.0.2): + - Flutter + - DKImagePickerController/Core (4.3.9): + - DKImagePickerController/ImageDataManager + - DKImagePickerController/Resource + - DKImagePickerController/ImageDataManager (4.3.9) + - DKImagePickerController/PhotoGallery (4.3.9): + - DKImagePickerController/Core + - DKPhotoGallery + - DKImagePickerController/Resource (4.3.9) + - DKPhotoGallery (0.0.19): + - DKPhotoGallery/Core (= 0.0.19) + - DKPhotoGallery/Model (= 0.0.19) + - DKPhotoGallery/Preview (= 0.0.19) + - DKPhotoGallery/Resource (= 0.0.19) + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Core (0.0.19): + - DKPhotoGallery/Model + - DKPhotoGallery/Preview + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Model (0.0.19): + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Preview (0.0.19): + - DKPhotoGallery/Model + - DKPhotoGallery/Resource + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Resource (0.0.19): + - SDWebImage + - SwiftyGif + - file_picker (0.0.1): + - DKImagePickerController/PhotoGallery + - Flutter + - Flutter (1.0.0) + - image_picker_ios (0.0.1): + - Flutter + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - powersync-sqlite-core (0.3.7) + - powersync_flutter_libs (0.0.1): + - Flutter + - powersync-sqlite-core (~> 0.3.6) + - SDWebImage (5.20.0): + - SDWebImage/Core (= 5.20.0) + - SDWebImage/Core (5.20.0) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (3.47.2): + - sqlite3/common (= 3.47.2) + - sqlite3/common (3.47.2) + - sqlite3/dbstatvtab (3.47.2): + - sqlite3/common + - sqlite3/fts5 (3.47.2): + - sqlite3/common + - sqlite3/perf-threadsafe (3.47.2): + - sqlite3/common + - sqlite3/rtree (3.47.2): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (~> 3.47.2) + - sqlite3/dbstatvtab + - sqlite3/fts5 + - sqlite3/perf-threadsafe + - sqlite3/rtree + - SwiftyGif (5.4.5) + - url_launcher_ios (0.0.1): + - Flutter + +DEPENDENCIES: + - app_links (from `.symlinks/plugins/app_links/ios`) + - file_picker (from `.symlinks/plugins/file_picker/ios`) + - Flutter (from `Flutter`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - powersync_flutter_libs (from `.symlinks/plugins/powersync_flutter_libs/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + +SPEC REPOS: + trunk: + - DKImagePickerController + - DKPhotoGallery + - powersync-sqlite-core + - SDWebImage + - sqlite3 + - SwiftyGif + +EXTERNAL SOURCES: + app_links: + :path: ".symlinks/plugins/app_links/ios" + file_picker: + :path: ".symlinks/plugins/file_picker/ios" + Flutter: + :path: Flutter + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + powersync_flutter_libs: + :path: ".symlinks/plugins/powersync_flutter_libs/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + sqlite3_flutter_libs: + :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + +SPEC CHECKSUMS: + app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0 + DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c + DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 + file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + powersync-sqlite-core: b9bcd19fa191d3d6f3f85c0573c41799d654cfa7 + powersync_flutter_libs: eb2694b322e1e4ef9a0f1e32dee600847f21ccbc + SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sqlite3: 7559e33dae4c78538df563795af3a86fc887ee71 + sqlite3_flutter_libs: 58ae36c0dd086395d066b4fe4de9cdca83e717b3 + SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + +PODFILE CHECKSUM: 9d8d1770d47a428fcb57a5d5f228a34e6d072ae7 + +COCOAPODS: 1.16.2 diff --git a/demos/supabase-trello/ios/Runner.xcodeproj/project.pbxproj b/demos/supabase-trello/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..643e5165 --- /dev/null +++ b/demos/supabase-trello/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,723 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 580DE0ECE5B255011C07A13C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E2B5432F50F552DF6C1D0FA1 /* Pods_Runner.framework */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 9F5E6CDBDFD984AD1CE17EBF /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C37744B39F4E3B36A0BBB0CE /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0FF24B675B56F022C3289F08 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 36C14A9DF8CBED78B8383618 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 976B259CF18C36EB3B6F4F0F /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A2E15785CC927C31AEF6BC6A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + B0E8CE81B52F70CDF2A4F80B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + C37744B39F4E3B36A0BBB0CE /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DB8B11F7834AB262F47996E5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + E2B5432F50F552DF6C1D0FA1 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 580DE0ECE5B255011C07A13C /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A9A2ECF3A7DDE6AA422CD674 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9F5E6CDBDFD984AD1CE17EBF /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + A7F3D089A45655143E0E6848 /* Pods */, + B312AFB09377AB967113F8FE /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + A7F3D089A45655143E0E6848 /* Pods */ = { + isa = PBXGroup; + children = ( + DB8B11F7834AB262F47996E5 /* Pods-Runner.debug.xcconfig */, + 36C14A9DF8CBED78B8383618 /* Pods-Runner.release.xcconfig */, + 0FF24B675B56F022C3289F08 /* Pods-Runner.profile.xcconfig */, + A2E15785CC927C31AEF6BC6A /* Pods-RunnerTests.debug.xcconfig */, + B0E8CE81B52F70CDF2A4F80B /* Pods-RunnerTests.release.xcconfig */, + 976B259CF18C36EB3B6F4F0F /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + B312AFB09377AB967113F8FE /* Frameworks */ = { + isa = PBXGroup; + children = ( + E2B5432F50F552DF6C1D0FA1 /* Pods_Runner.framework */, + C37744B39F4E3B36A0BBB0CE /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 579312ED36242754EF3992BA /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + A9A2ECF3A7DDE6AA422CD674 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 5582B600ABA2BF2B04110F91 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + B27A73ED59948A115A9CD70F /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 5582B600ABA2BF2B04110F91 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 579312ED36242754EF3992BA /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + B27A73ED59948A115A9CD70F /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = K2CV557XCP; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.journeyapps.trelloappcloneFlutter; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A2E15785CC927C31AEF6BC6A /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.trelloappcloneFlutter.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B0E8CE81B52F70CDF2A4F80B /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.trelloappcloneFlutter.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 976B259CF18C36EB3B6F4F0F /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.trelloappcloneFlutter.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = K2CV557XCP; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.journeyapps.trelloappcloneFlutter; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = K2CV557XCP; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.journeyapps.trelloappcloneFlutter; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/demos/supabase-trello/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/demos/supabase-trello/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/demos/supabase-trello/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/demos/supabase-trello/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demos/supabase-trello/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/demos/supabase-trello/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/demos/supabase-trello/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/demos/supabase-trello/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/demos/supabase-trello/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/demos/supabase-trello/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demos/supabase-trello/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..8e3ca5df --- /dev/null +++ b/demos/supabase-trello/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/supabase-trello/ios/Runner.xcworkspace/contents.xcworkspacedata b/demos/supabase-trello/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/demos/supabase-trello/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/demos/supabase-trello/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demos/supabase-trello/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/demos/supabase-trello/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/demos/supabase-trello/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/demos/supabase-trello/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/demos/supabase-trello/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/demos/supabase-trello/ios/Runner/AppDelegate.swift b/demos/supabase-trello/ios/Runner/AppDelegate.swift new file mode 100644 index 00000000..70693e4a --- /dev/null +++ b/demos/supabase-trello/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d36b1fab --- /dev/null +++ b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 00000000..dc9ada47 Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 00000000..7353c41e Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 00000000..797d452e Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 00000000..6ed2d933 Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 00000000..4cd7b009 Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 00000000..fe730945 Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 00000000..321773cd Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 00000000..797d452e Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 00000000..502f463a Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 00000000..0ec30343 Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 00000000..0ec30343 Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 00000000..e9f5fea2 Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 00000000..84ac32ae Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 00000000..8953cba0 Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 00000000..0467bf12 Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 00000000..0bedcf2f --- /dev/null +++ b/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 00000000..89c2725b --- /dev/null +++ b/demos/supabase-trello/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/demos/supabase-trello/ios/Runner/Base.lproj/LaunchScreen.storyboard b/demos/supabase-trello/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..f2e259c7 --- /dev/null +++ b/demos/supabase-trello/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/supabase-trello/ios/Runner/Base.lproj/Main.storyboard b/demos/supabase-trello/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f3c28516 --- /dev/null +++ b/demos/supabase-trello/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/supabase-trello/ios/Runner/Info.plist b/demos/supabase-trello/ios/Runner/Info.plist new file mode 100644 index 00000000..22057481 --- /dev/null +++ b/demos/supabase-trello/ios/Runner/Info.plist @@ -0,0 +1,51 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Trelloappclone Flutter + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + trelloappclone_flutter + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/demos/supabase-trello/ios/Runner/Runner-Bridging-Header.h b/demos/supabase-trello/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 00000000..308a2a56 --- /dev/null +++ b/demos/supabase-trello/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/demos/supabase-trello/ios/RunnerTests/RunnerTests.swift b/demos/supabase-trello/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..86a7c3b1 --- /dev/null +++ b/demos/supabase-trello/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/demos/supabase-trello/lib/features/aboutboard/presentation/index.dart b/demos/supabase-trello/lib/features/aboutboard/presentation/index.dart new file mode 100644 index 00000000..4f089aea --- /dev/null +++ b/demos/supabase-trello/lib/features/aboutboard/presentation/index.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/constant.dart'; + +class AboutBoard extends StatefulWidget { + const AboutBoard({super.key}); + + @override + State createState() => _AboutBoardState(); +} + +class _AboutBoardState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("About this board"), + centerTitle: false, + ), + body: const Padding( + padding: EdgeInsets.all(20.0), + child: Column(children: [ + Text( + "Made by", + style: TextStyle(fontWeight: FontWeight.w600), + ), + ListTile( + leading: CircleAvatar(), + title: Text("Jane Doe"), + subtitle: Text("@janedoe"), + ), + Text( + "Description", + style: TextStyle(fontWeight: FontWeight.w600), + ), + Padding( + padding: EdgeInsets.only(top: 10.0), + child: Text(defaultDescription), + ) + ]), + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/activity/presentation/index.dart b/demos/supabase-trello/lib/features/activity/presentation/index.dart new file mode 100644 index 00000000..6a15a08c --- /dev/null +++ b/demos/supabase-trello/lib/features/activity/presentation/index.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; +import 'package:trelloappclone_flutter/models/activity.dart'; +import 'package:trelloappclone_flutter/models/card.dart'; + +import '../../../utils/service.dart'; + +class Activities extends StatefulWidget { + final Cardlist crd; + const Activities(this.crd, {super.key}); + + @override + State createState() => _ActivitiesState(); +} + +class _ActivitiesState extends State with Service { + List activities = []; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + initialData: activities, + future: getActivities(widget.crd), + builder: + (BuildContext context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + List children = snapshot.data as List; + + if (children.isNotEmpty) { + return ListView( + shrinkWrap: true, children: buildWidget(children)); + } + } + return const SizedBox.shrink(); + }); + } + + List buildWidget(List activities) { + List tiles = []; + + for (int i = 0; i < activities.length; i++) { + tiles.add(ActivityTile(activity: activities[i].description)); + } + return tiles; + } +} + +class ActivityTile extends StatefulWidget { + final String activity; + const ActivityTile({required this.activity, super.key}); + + @override + State createState() => _ActivityTileState(); +} + +class _ActivityTileState extends State { + @override + Widget build(BuildContext context) { + return ListTile( + leading: const CircleAvatar( + backgroundColor: brandColor, + ), + title: Text(widget.activity), + subtitle: const Text("01 Jan 2023 at 1:11 am"), + ); + } +} diff --git a/demos/supabase-trello/lib/features/archivedcards/presentation/index.dart b/demos/supabase-trello/lib/features/archivedcards/presentation/index.dart new file mode 100644 index 00000000..5702546d --- /dev/null +++ b/demos/supabase-trello/lib/features/archivedcards/presentation/index.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +import '../../../utils/color.dart'; + +class ArchivedCards extends StatefulWidget { + const ArchivedCards({super.key}); + + @override + State createState() => _ArchivedCardsState(); +} + +class _ArchivedCardsState extends State { + bool select = false; + int selected = 0; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: (select) + ? IconButton( + onPressed: () {}, + icon: const Icon( + Icons.close, + size: 30, + ), + ) + : null, + title: Text((select) ? "&selected selected" : "Archived cards"), + centerTitle: false, + actions: [ + (select) + ? TextButton( + onPressed: () {}, + child: const Text( + "SEND TO BOARD", + style: TextStyle(color: whiteShade), + )) + : IconButton( + onPressed: () {}, + icon: const Icon(Icons.check_circle_outline)) + ], + ), + body: const Center( + child: Text("No archived cards"), + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/archivedlists/presentation/index.dart b/demos/supabase-trello/lib/features/archivedlists/presentation/index.dart new file mode 100644 index 00000000..76f5181c --- /dev/null +++ b/demos/supabase-trello/lib/features/archivedlists/presentation/index.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; + +class ArchivedLists extends StatefulWidget { + const ArchivedLists({super.key}); + + @override + State createState() => _ArchivedListsState(); +} + +class _ArchivedListsState extends State { + bool select = false; + int selected = 0; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: (select) + ? IconButton( + onPressed: () {}, + icon: const Icon( + Icons.close, + size: 30, + ), + ) + : null, + title: Text((select) ? "&selected selected" : "Archived lists"), + centerTitle: false, + actions: [ + (select) + ? TextButton( + onPressed: () {}, + child: const Text( + "SEND TO BOARD", + style: TextStyle(color: whiteShade), + )) + : IconButton( + onPressed: () {}, + icon: const Icon(Icons.check_circle_outline)) + ], + ), + body: const Center( + child: Text("No archived lists"), + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/board/domain/board_arguments.dart b/demos/supabase-trello/lib/features/board/domain/board_arguments.dart new file mode 100644 index 00000000..7b7720d5 --- /dev/null +++ b/demos/supabase-trello/lib/features/board/domain/board_arguments.dart @@ -0,0 +1,9 @@ +import 'package:trelloappclone_flutter/models/board.dart'; +import 'package:trelloappclone_flutter/models/workspace.dart'; + +class BoardArguments { + final Board board; + final Workspace workspace; + + BoardArguments(this.board, this.workspace); +} diff --git a/demos/supabase-trello/lib/features/board/presentation/boarditemobject.dart b/demos/supabase-trello/lib/features/board/presentation/boarditemobject.dart new file mode 100644 index 00000000..62f5b5b4 --- /dev/null +++ b/demos/supabase-trello/lib/features/board/presentation/boarditemobject.dart @@ -0,0 +1,13 @@ +import 'package:trelloappclone_flutter/models/card_label.dart'; + +class BoardItemObject { + String? title; + bool? hasDescription; + List? cardLabels; + + BoardItemObject({this.title, this.hasDescription, this.cardLabels}) { + title ??= ""; + hasDescription ??= false; + cardLabels ??= []; + } +} diff --git a/demos/supabase-trello/lib/features/board/presentation/boardlistobject.dart b/demos/supabase-trello/lib/features/board/presentation/boardlistobject.dart new file mode 100644 index 00000000..0b19b482 --- /dev/null +++ b/demos/supabase-trello/lib/features/board/presentation/boardlistobject.dart @@ -0,0 +1,13 @@ +import 'boarditemobject.dart'; + +class BoardListObject { + String? title; + String? listId; + List? items; + + BoardListObject({this.title, this.listId, this.items}) { + listId ??="0"; + title ??= ""; + items ??= []; + } +} diff --git a/demos/supabase-trello/lib/features/board/presentation/index.dart b/demos/supabase-trello/lib/features/board/presentation/index.dart new file mode 100644 index 00000000..b71b38a8 --- /dev/null +++ b/demos/supabase-trello/lib/features/board/presentation/index.dart @@ -0,0 +1,471 @@ +import 'dart:ffi'; + +import 'package:flutter/material.dart'; +import 'package:status_alert/status_alert.dart'; +import 'package:trelloappclone_flutter/features/carddetails/domain/card_detail_arguments.dart'; +import 'package:trelloappclone_flutter/features/carddetails/presentation/index.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; +import 'package:trelloappclone_flutter/widgets/thirdparty/board_item.dart'; +import 'package:trelloappclone_flutter/widgets/thirdparty/board_list.dart'; +import 'package:trelloappclone_flutter/widgets/thirdparty/boardview.dart'; +import 'package:trelloappclone_flutter/widgets/thirdparty/boardview_controller.dart'; +import 'package:trelloappclone_flutter/models/listboard.dart'; +import 'package:trelloappclone_flutter/models/card.dart'; + +import '../../../main.dart'; +import '../../../utils/config.dart'; +import '../../../utils/service.dart'; +import '../../../utils/widgets.dart'; +import '../domain/board_arguments.dart'; +import 'boarditemobject.dart'; +import 'boardlistobject.dart'; + +class BoardScreen extends StatefulWidget { + const BoardScreen({super.key}); + + @override + State createState() => _BoardScreenState(); + + static const routeName = '/board'; +} + +class _BoardScreenState extends State with Service { + BoardViewController boardViewController = BoardViewController(); + bool showCard = false; + bool show = false; + List lists = []; + final TextEditingController nameController = TextEditingController(); + Map textEditingControllers = {}; + Map showtheCard = {}; + int selectedList = 0; + int selectedCard = 0; + late double width; + + @override + Widget build(BuildContext context) { + width = MediaQuery.of(context).size.width * 0.7; + final args = ModalRoute.of(context)!.settings.arguments as BoardArguments; + trello.setSelectedBoard(args.board); + trello.setSelectedWorkspace(args.workspace); + + return WillPopScope( + onWillPop: () async { + Navigator.pushNamed(context, "/home"); + return false; + }, + child: Scaffold( + appBar: (!show && !showCard) + ? AppBar( + backgroundColor: brandColor, + centerTitle: false, + title: Text(args.board.name), + actions: [ + IconButton( + onPressed: () { + Navigator.pushNamed(context, '/boardmenu'); + }, + icon: const Icon(Icons.more_horiz)) + ], + ) + : AppBar( + leading: IconButton( + onPressed: () { + setState(() { + nameController.clear(); + textEditingControllers[selectedList]!.clear(); + show = false; + showCard = false; + showtheCard[selectedCard] = false; + }); + }, + icon: const Icon(Icons.close)), + title: Text((show) ? "Add list" : "Add card"), + centerTitle: false, + actions: [ + IconButton( + onPressed: () { + if (show) { + addList(Listboard( + id: randomUuid(), + workspaceId: args.workspace.id, + boardId: args.board.id, + userId: trello.user.id, + name: nameController.text, + order: trello.lstbrd.length)); + nameController.clear(); + setState(() { + show = false; + }); + } else { + addCard(Cardlist( + id: randomUuid(), + workspaceId: args.workspace.id, + listId: trello.lstbrd[selectedList].id, + userId: trello.user.id, + name: + textEditingControllers[selectedList]!.text, + rank: + trello.lstbrd[selectedList].cards!.length)); + textEditingControllers[selectedList]!.clear(); + setState(() { + showCard = false; + showtheCard[selectedCard] = false; + }); + } + }, + icon: const Icon(Icons.check)) + ], + ), + body: Padding( + padding: const EdgeInsets.all(10.0), + child: StreamBuilder( + stream: getListsByBoardStream(args.board), + builder: (BuildContext context, + AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + List listBoards = + snapshot.data as List; + return BoardView( + lists: loadBoardView(listBoards), + boardViewController: boardViewController, + ); + } + return const SizedBox.shrink(); + })), + )); + } + + Widget buildBoardItem( + BoardItemObject itemObject, List data) { + return BoardItem( + onStartDragItem: (listIndex, itemIndex, state) {}, + onDropItem: (listIndex, itemIndex, oldListIndex, oldItemIndex, state) { + // if listIndex is null, then item was dropped outside of list reset the state + if (listIndex == null) { + return; + } + + if (itemIndex == null || itemIndex > data[listIndex].items!.length) { + return; + } + + // Move item to new list + var item = data[oldListIndex!].items?[oldItemIndex!]; + data[oldListIndex].items!.removeAt(oldItemIndex!); + data[listIndex].items!.insert(itemIndex, item!); + + var card = trello.lstbrd[oldListIndex].cards![oldItemIndex]; + + // update card listId + card.listId = trello.lstbrd[listIndex].id; + updateCard(card); + + trello.lstbrd[oldListIndex].cards!.removeAt(oldItemIndex); + trello.lstbrd[listIndex].cards!.insert(itemIndex, card); + + // reset rank based on index + trello.lstbrd[listIndex].cards!.asMap().forEach((index, card) { + card.rank = index; + updateCard(card); + }); + }, + onTapItem: (listIndex, itemIndex, state) { + Navigator.pushNamed(context, CardDetails.routeName, + arguments: CardDetailArguments( + trello.lstbrd[listIndex].cards![itemIndex], + trello.selectedBoard, + trello.lstbrd[listIndex])) + .then((value) => setState(() {})); + }, + item: Card( + color: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + child: Column(children: [ + Padding( + padding: const EdgeInsets.fromLTRB(10, 8, 8, 8), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + itemObject.title!, + ), + ), + ), + Wrap( + children: [ + // Add a horizontal space + const SizedBox(width: 0), + // Example labels with colored Chips + ...itemObject.cardLabels!.map((cardLabel) => Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4, vertical: 2), // Horizontal margin + child: LabelDiplay( + color: trello.selectedBoard.boardLabels! + .firstWhere((boardLabel) => + boardLabel.id == cardLabel.boardLabelId) + .color, + label: trello.selectedBoard.boardLabels! + .firstWhere((boardLabel) => + boardLabel.id == cardLabel.boardLabelId) + .title))), + ], + ), + //Add icon to the column if card has description + if (itemObject.hasDescription!) + const Padding( + padding: EdgeInsets.fromLTRB(8, 2, 8, 8), + child: Align( + alignment: Alignment.centerLeft, + child: Icon(Icons.description, size: 16), + ), + ), + ]))); + } + + Widget _createBoardList( + BoardListObject list, List data, int index) { + List items = []; + for (int i = 0; i < list.items!.length; i++) { + items.insert(i, buildBoardItem(list.items![i], data) as BoardItem); + } + + textEditingControllers.putIfAbsent(index, () => TextEditingController()); + showtheCard.putIfAbsent(index, () => false); + + items.insert( + list.items!.length, + BoardItem( + onTapItem: (listIndex, itemIndex, state) { + setState(() { + selectedList = listIndex!; + selectedCard = index; + showCard = true; + showtheCard[index] = true; + }); + }, + item: (!showtheCard[index]!) + ? ListTile( + leading: const Text.rich(TextSpan( + children: [ + WidgetSpan( + child: Icon( + Icons.add, + size: 19, + color: whiteShade, + )), + WidgetSpan( + child: SizedBox( + width: 5, + ), + ), + TextSpan( + text: "Add card", + style: TextStyle(color: whiteShade)), + ], + )), + trailing: IconButton( + icon: const Icon( + Icons.image, + color: whiteShade, + ), + onPressed: () {}, + ), + ) + : Padding( + padding: const EdgeInsets.all(10.0), + child: TextField( + controller: textEditingControllers[index], + decoration: const InputDecoration(hintText: "Card name"), + ), + ), + )); + + return BoardList( + onStartDragList: (listIndex) {}, + onTapList: (listIndex) async {}, + onDropList: (listIndex, oldListIndex) { + var tmpList = data[oldListIndex!]; + + data.removeAt(oldListIndex); + data.insert(listIndex!, tmpList); + + updateListOrder(tmpList.listId!, listIndex); + + var movedList = trello.lstbrd[oldListIndex]; + + trello.lstbrd.removeAt(oldListIndex); + trello.lstbrd.insert(listIndex, movedList); + + // reset rank based on index + trello.lstbrd.asMap().forEach((index, list) { + updateListOrder(list.id, index); + }); + }, + headerBackgroundColor: brandColor, + backgroundColor: brandColor, + header: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(5), + child: ListTile( + leading: SizedBox( + width: 180, + child: Text( + overflow: TextOverflow.ellipsis, + softWrap: false, + maxLines: 2, + list.title!, + style: const TextStyle( + fontSize: 16, fontWeight: FontWeight.w500), + ), + ), + trailing: PopupMenuButton( + child: const Icon(Icons.more_vert), + itemBuilder: (BuildContext context) => + >[ + PopupMenuItem( + child: ListTile( + enabled: false, + title: Text(listMenu[1]), + ), + ), + PopupMenuItem( + child: ListTile( + enabled: false, + title: Text(listMenu[2]), + ), + ), + PopupMenuItem( + child: ListTile( + enabled: false, + title: Text(listMenu[3]), + ), + ), + const PopupMenuItem( + child: Divider( + height: 1, + thickness: 1, + )), + PopupMenuItem( + child: ListTile( + enabled: false, + title: Text(listMenu[4]), + trailing: + const Icon(Icons.keyboard_arrow_right), + ), + ), + const PopupMenuItem( + child: Divider( + height: 1, + thickness: 1, + )), + PopupMenuItem( + child: ListTile( + enabled: false, + title: Text(listMenu[5]), + ), + ), + PopupMenuItem( + child: ListTile( + title: Text(listMenu[6]), + onTap: () { + archiveCardsInList(trello.lstbrd[index]) + .then((numCardsArchived) { + StatusAlert.show(context, + duration: const Duration(seconds: 2), + title: + '$numCardsArchived Cards Archived', + configuration: const IconConfiguration( + icon: Icons.archive_outlined, + color: brandColor), + maxWidth: 260); + Navigator.of(context).pop(); + }); + }, + ), + ), + PopupMenuItem( + child: ListTile( + enabled: false, + title: Text(listMenu[7]), + ), + ), + ]), + ))), + ], + items: items, + ); + } + + List generateBoardListObject(List lists) { + final List listData = []; + + for (int i = 0; i < lists.length; i++) { + listData.add(BoardListObject( + title: lists[i].name, + listId: lists[i].id, + items: generateBoardItemObject(lists[i].cards!))); + } + + return listData; + } + + List generateBoardItemObject(List crds) { + final List items = []; + for (int i = 0; i < crds.length; i++) { + items.add(BoardItemObject( + title: crds[i].name, + cardLabels: crds[i].cardLabels, + hasDescription: (crds[i].description != null) ? true : false)); + } + return items; + } + + List loadBoardView(List Listboards) { + List data = generateBoardListObject(Listboards); + lists = []; + + for (int i = 0; i < data.length; i++) { + lists.add(_createBoardList(data[i], data, i) as BoardList); + } + + lists.insert( + data.length, + BoardList( + items: [ + BoardItem( + item: GestureDetector( + onTap: () { + setState(() { + show = true; + }); + }, + child: Container( + alignment: Alignment.center, + width: width, + height: 50, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + color: brandColor, + ), + child: (!show) + ? const Text( + "Add list", + style: TextStyle(color: whiteShade), + ) + : Padding( + padding: const EdgeInsets.all(10.0), + child: TextField( + controller: nameController, + decoration: + const InputDecoration(hintText: "List name"), + ), + )), + )) + ], + )); + return lists; + } +} diff --git a/demos/supabase-trello/lib/features/boardbackground/presentation/index.dart b/demos/supabase-trello/lib/features/boardbackground/presentation/index.dart new file mode 100644 index 00000000..6b08897a --- /dev/null +++ b/demos/supabase-trello/lib/features/boardbackground/presentation/index.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/config.dart'; +import '../../../main.dart'; + +class BoardBackground extends StatefulWidget { + const BoardBackground({super.key}); + + @override + State createState() => _BoardBackgroundState(); +} + +class _BoardBackgroundState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Board background"), + centerTitle: false, + ), + body: GridView.builder( + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + childAspectRatio: 1, + crossAxisSpacing: 3, + mainAxisSpacing: 20), + itemCount: backgrounds.length, + itemBuilder: (BuildContext cxt, index) { + return GestureDetector( + onTap: () { + setState(() { + trello.setSelectedBg(backgrounds[index]); + }); + }, + child: Stack( + children: [ + Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: Color(int.parse( + backgrounds[index].substring(1, 7), + radix: 16) + + 0xff000000), + borderRadius: BorderRadius.circular(5)), + ), + (backgrounds[index] == trello.selectedBackground) + ? const Center( + child: Icon( + Icons.check, + color: Colors.white, + size: 50, + ), + ) + : const SizedBox.shrink() + ], + )); + }), + ); + } +} diff --git a/demos/supabase-trello/lib/features/boardmenu/presentation/index.dart b/demos/supabase-trello/lib/features/boardmenu/presentation/index.dart new file mode 100644 index 00000000..82badf5b --- /dev/null +++ b/demos/supabase-trello/lib/features/boardmenu/presentation/index.dart @@ -0,0 +1,202 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/features/visibility/presentation/index.dart'; +import 'package:trelloappclone_flutter/main.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; + +class BoardMenu extends StatefulWidget { + const BoardMenu({super.key}); + + @override + State createState() => _BoardMenuState(); +} + +class _BoardMenuState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: const Icon(Icons.close), + ), + title: const Text("Board menu"), + centerTitle: false, + ), + body: SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(20.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + decoration: const BoxDecoration( + color: brandColor, + borderRadius: BorderRadius.all(Radius.circular(10.0))), + child: IconButton( + onPressed: () {}, + icon: const Icon( + Icons.star_border, + size: 30, + ), + ), + ), + Container( + decoration: const BoxDecoration( + color: brandColor, + borderRadius: BorderRadius.all(Radius.circular(10.0))), + child: IconButton( + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return const BoardVisibility(); + }); + }, + icon: const Icon( + Icons.people, + size: 30, + ), + ), + ), + Container( + decoration: const BoxDecoration( + color: brandColor, + borderRadius: BorderRadius.all(Radius.circular(10.0))), + child: IconButton( + onPressed: () { + Navigator.pushNamed(context, "/copyboard"); + }, + icon: const Icon( + Icons.copy, + size: 30, + ), + ), + ), + Container( + decoration: const BoxDecoration( + color: brandColor, + borderRadius: BorderRadius.all(Radius.circular(10.0))), + child: IconButton( + onPressed: () { + Navigator.pushNamed(context, "/boardsettings"); + }, + icon: const Icon( + Icons.more_horiz, + size: 30, + ), + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.only(top: 10.0), + child: ListTile( + tileColor: whiteShade, + leading: const Icon(Icons.person_outline), + title: const Padding( + padding: EdgeInsets.only(top: 15.0, bottom: 15.0), + child: Text("Members"), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 18.0), + child: InkWell( + onTap: () { + Navigator.pushNamed(context, '/members'); + }, + child: Row( + children: buildMemberAvatars(), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 15.0), + child: SizedBox( + height: 37, + width: MediaQuery.of(context).size.width * 0.7, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: brandColor), + onPressed: () { + Navigator.pushNamed(context, "/invitemember"); + }, + child: const Text("Invite to workspace"), + ), + ), + ) + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 15.0), + child: Container( + color: whiteShade, + child: ListTile( + leading: const Icon(Icons.info_outline), + title: const Text("About this board"), + onTap: () { + Navigator.pushNamed(context, '/aboutboard'); + }, + ), + ), + ), + // Padding( + // padding: const EdgeInsets.only(top: 15.0), + // child: Container( + // color: whiteShade, + // child: ListTile( + // leading: const Icon(Icons.rocket), + // title: const Text("Power-Ups"), + // onTap: () { + // Navigator.pushNamed(context, '/powerups'); + // }, + // ), + // )), + // Padding( + // padding: const EdgeInsets.only(top: 15.0), + // child: Container( + // color: whiteShade, + // child: ListTile( + // leading: const Icon(Icons.push_pin_outlined), + // title: const Text("Pin to home screen"), + // onTap: () {}, + // ), + // )), + // const Padding( + // padding: EdgeInsets.all(15.0), + // child: Text( + // "Activity", + // style: TextStyle(fontWeight: FontWeight.bold), + // ), + // ), + // //TODO: figure out what is going on here + // Activities(Cardlist( + // id: "todo", workspaceId: trello.selectedWorkspace.id, listId: "todo", userId: trello.user.id, name: "")) + ], + )), + ); + } + + List buildMemberAvatars() { + List avatars = []; + + trello.selectedWorkspace.members?.forEach((member) { + avatars.add(CircleAvatar( + backgroundColor: brandColor, + child: Text(member.name[0].toUpperCase()), + )); + avatars.add(const SizedBox( + width: 4, + )); + }); + return avatars; + } +} diff --git a/demos/supabase-trello/lib/features/boardsettings/presentation/index.dart b/demos/supabase-trello/lib/features/boardsettings/presentation/index.dart new file mode 100644 index 00000000..b3826325 --- /dev/null +++ b/demos/supabase-trello/lib/features/boardsettings/presentation/index.dart @@ -0,0 +1,206 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; +import 'package:trelloappclone_flutter/utils/config.dart'; + +import '../../../utils/widgets.dart'; +import '../../closeboard/presentation/index.dart'; + +class BoardSettings extends StatefulWidget { + const BoardSettings({super.key}); + + @override + State createState() => _BoardSettingsState(); +} + +class _BoardSettingsState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text("Board settings")), + body: SingleChildScrollView( + child: Column( + children: [ + const BlueRectangle(), + Padding( + padding: const EdgeInsets.only(top: 15.0), + child: Container( + color: whiteShade, + child: const ListTile( + leading: Text("Name"), + trailing: Text("Board 1"), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 3.0), + child: Container( + color: whiteShade, + child: const ListTile( + leading: Text("Workspace"), + trailing: Text("Workspace 1"), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 3.0), + child: Container( + color: whiteShade, + child: ListTile( + leading: const Text("Background"), + trailing: ColorSquare( + bckgrd: backgrounds[0], + ), + onTap: () { + Navigator.pushNamed(context, "/boardbackground"); + }, + ), + ), + ), + // Padding( + // padding: const EdgeInsets.only(top: 3.0), + // child: Container( + // color: whiteShade, + // child: ListTile( + // leading: const Text("Enable card cover images"), + // trailing: Switch(value: true, onChanged: ((value) {})), + // ), + // ), + // ), + // Padding( + // padding: const EdgeInsets.only(top: 3.0), + // child: Container( + // color: whiteShade, + // child: ListTile( + // leading: const Text("Watch"), + // trailing: Switch(value: false, onChanged: ((value) {}))), + // ), + // ), + // Padding( + // padding: const EdgeInsets.only(top: 3.0), + // child: Container( + // color: whiteShade, + // child: ListTile( + // leading: const Text("Available offline"), + // trailing: Switch(value: false, onChanged: ((value) {}))), + // ), + // ), + // Padding( + // padding: const EdgeInsets.only(top: 3.0), + // child: Container( + // color: whiteShade, + // child: ListTile( + // leading: const Text("Edit labels"), + // onTap: () { + // showDialog( + // context: context, + // builder: (BuildContext context) { + // return const EditLabels(); + // }); + // }, + // ), + // ), + // ), + // Padding( + // padding: const EdgeInsets.only(top: 3.0), + // child: Container( + // color: whiteShade, + // child: ListTile( + // leading: const Text("Email-to-board settings"), + // onTap: () { + // Navigator.pushNamed(context, "/emailtoboard"); + // }, + // ), + // ), + // ), + // Padding( + // padding: const EdgeInsets.only(top: 3.0), + // child: Container( + // color: whiteShade, + // child: ListTile( + // leading: const Text("Archived cards"), + // onTap: () { + // Navigator.pushNamed(context, "/archivedcards"); + // }, + // ), + // ), + // ), + // Padding( + // padding: const EdgeInsets.only(top: 3.0), + // child: Container( + // color: whiteShade, + // child: ListTile( + // leading: const Text("Archived lists"), + // onTap: () { + // Navigator.pushNamed(context, "/archivedlists"); + // }, + // ), + // ), + // ), + Padding( + padding: const EdgeInsets.only(top: 15.0), + child: Container( + color: whiteShade, + child: const ListTile( + leading: Text("Visibility"), + trailing: Text("Public"), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 3.0), + child: Container( + color: whiteShade, + child: const ListTile( + leading: Text("Commenting"), + trailing: Text("Members"), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 3.0), + child: Container( + color: whiteShade, + child: const ListTile( + leading: Text("Adding members"), + trailing: Text("Members"), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 15.0), + child: Container( + color: whiteShade, + child: ListTile( + leading: const Text("Self join"), + trailing: Switch( + value: true, + onChanged: ((value) {}), + )), + ), + ), + const Padding( + padding: EdgeInsets.only(top: 10.0), + child: + Text("Any Workspace member can edit and join the board")), + Padding( + padding: const EdgeInsets.only(top: 15.0, bottom: 50), + child: Container( + color: whiteShade, + child: ListTile( + leading: const Text("Close board"), + onTap: () { + showDialog( + context: context, + builder: (BuildContext context) { + return const CloseBoard(); + }); + }, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/carddetails/domain/card_detail_arguments.dart b/demos/supabase-trello/lib/features/carddetails/domain/card_detail_arguments.dart new file mode 100644 index 00000000..b76dfd32 --- /dev/null +++ b/demos/supabase-trello/lib/features/carddetails/domain/card_detail_arguments.dart @@ -0,0 +1,11 @@ +import 'package:trelloappclone_flutter/models/board.dart'; +import 'package:trelloappclone_flutter/models/card.dart'; +import 'package:trelloappclone_flutter/models/listboard.dart'; + +class CardDetailArguments { + final Cardlist crd; + final Board brd; + final Listboard lst; + + CardDetailArguments(this.crd, this.brd, this.lst); +} diff --git a/demos/supabase-trello/lib/features/carddetails/presentation/index.dart b/demos/supabase-trello/lib/features/carddetails/presentation/index.dart new file mode 100644 index 00000000..4249b071 --- /dev/null +++ b/demos/supabase-trello/lib/features/carddetails/presentation/index.dart @@ -0,0 +1,376 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/features/activity/presentation/index.dart'; +import 'package:trelloappclone_flutter/models/checklist.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; +import 'package:trelloappclone_flutter/main.dart'; + +import '../../../utils/service.dart'; +import '../../../utils/widgets.dart'; +import '../../editlabels/presentation/index.dart'; +import '../../viewmembers/presentation/index.dart'; +import '../domain/card_detail_arguments.dart'; + +class CardDetails extends StatefulWidget { + const CardDetails({super.key}); + + @override + State createState() => _CardDetailsState(); + + static const routeName = '/carddetail'; +} + +class _CardDetailsState extends State with Service { + final TextEditingController descriptionController = TextEditingController(); + final TextEditingController nameController = TextEditingController(); + final TextEditingController checklistController = TextEditingController(); + bool showChecklist = false; + bool addCardDescription = false; + bool editCardName = false; + Map checked = {}; + + @override + Widget build(BuildContext context) { + final args = + ModalRoute.of(context)!.settings.arguments as CardDetailArguments; + + trello.setSelectedCard(args.crd); + descriptionController.text = args.crd.description ?? " "; + nameController.text = args.crd.name ?? " "; + + return Scaffold( + appBar: (showChecklist || addCardDescription || editCardName) + ? AppBar( + leading: IconButton( + onPressed: () { + setState(() { + showChecklist = false; + addCardDescription = false; + editCardName = false; + }); + }, + icon: const Icon(Icons.close, size: 30), + ), + title: Text(() { + if (showChecklist) { + return "Add Checklist"; + } else if (addCardDescription) { + return "Add card description"; + } else if (editCardName) { + return "Edit card name"; + } else { + return ""; + } + }()), + actions: [ + IconButton( + icon: const Icon(Icons.check), + onPressed: () { + if (showChecklist) { + createChecklist(Checklist( + id: randomUuid(), + workspaceId: args.crd.workspaceId, + cardId: args.crd.id, + name: checklistController.text, + status: false)); + checklistController.clear(); + setState(() { + showChecklist = false; + }); + } else if (addCardDescription || editCardName) { + if (addCardDescription && + descriptionController.text.isNotEmpty) { + args.crd.description = descriptionController.text; + } + + if (editCardName && nameController.text.isNotEmpty) { + args.crd.name = nameController.text; + } + + updateCard(args.crd); + descriptionController.clear(); + nameController.clear(); + setState(() { + addCardDescription = false; + editCardName = false; + }); + } + }, + ) + ]) + : AppBar( + leading: IconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: const Icon(Icons.close, size: 30), + ), + actions: [ + PopupMenuButton( + itemBuilder: (context) { + return [ + PopupMenuItem( + onTap: () => WidgetsBinding?.instance + ?.addPostFrameCallback((_) { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text('Delete Card'), + content: const Text( + 'Are you sure you want to delete this card?'), + actions: [ + TextButton( + onPressed: () => + Navigator.pop(context, 'Cancel'), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => { + deleteCard(args.crd), + // Remove popup + Navigator.pop(context, 'Delete'), + // Go one view back + Navigator.pop(context, 'Delete'), + }, + child: const Text('Delete'), + ), + ], + )); + }), + value: const Text("Delete Card"), + child: const ListTile( + leading: Icon(Icons.delete), + title: Text( + "Delete Card", + ), + ), + ), + ]; + }, + ) + // IconButton( + // icon: const Icon(Icons.more_vert), + // onPressed: () {}, + // ) + ]), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(10.0), + child: + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + ListTile( + title: TextField( + style: const TextStyle( + fontSize: 20.0, // Set your desired font size here + ), + controller: nameController, + onTap: () { + setState(() { + editCardName = true; + }); + }, + decoration: const InputDecoration(hintText: "Edit card name"), + ), + ), + RichText( + text: TextSpan( + text: args.brd.name, + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: themeColor), + children: [ + const TextSpan( + text: ' in list ', style: TextStyle(fontSize: 12)), + TextSpan(text: args.lst.name) + ])), + const Padding( + padding: EdgeInsets.only(top: 8.0, bottom: 8.0), + child: Text( + "Quick actions", + style: TextStyle(fontWeight: FontWeight.w600), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: Row( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width * 0.4, + child: ElevatedButton.icon( + onPressed: () { + setState(() { + showChecklist = true; + }); + }, + label: const Text("Add Checklist"), + icon: const CircleAvatar( + backgroundColor: brandColor, + radius: 15, + child: Icon(Icons.checklist), + ), + ), + ), + const Spacer(), + SizedBox( + width: MediaQuery.of(context).size.width * 0.4, + child: ElevatedButton.icon( + onPressed: null, + label: const Text("Add Attachment"), + icon: const CircleAvatar( + backgroundColor: brandColor, + radius: 15, + child: Icon(Icons.attachment), + ), + ), + ) + ], + ), + ), + ListTile( + leading: const Icon(Icons.short_text), + title: TextField( + controller: descriptionController, + keyboardType: TextInputType.multiline, + minLines: 1, + maxLines: 1024, + onTap: () { + setState(() { + addCardDescription = true; + }); + }, + decoration: + const InputDecoration(hintText: "Add card description"), + ), + ), + ListTile( + leading: const Icon(Icons.label), + title: Row( + children: [ + const Text("Labels"), + // Add a horizontal space + const SizedBox(width: 8), + // Example labels with colored Chips + ...trello.selectedCard!.cardLabels!.map( + (cardLabel) => Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4), // Horizontal margin + child: LabelDiplay( + color: trello.selectedBoard.boardLabels! + .firstWhere((boardLabel) => + boardLabel.id == cardLabel.boardLabelId) + .color, + label: trello.selectedBoard.boardLabels! + .firstWhere((boardLabel) => + boardLabel.id == cardLabel.boardLabelId) + .title)), + ), + ], + ), + onTap: () { + final result = showDialog( + context: context, + builder: (BuildContext context) { + return EditLabels(cardId: args.crd.id); + }); + + result.then((value) { + setState(() {}); + }); + }, + ), + ListTile( + leading: const Icon(Icons.person), + title: const Text("Members"), + onTap: () { + showDialog( + context: context, + builder: (BuildContext context) { + return const ViewMembers(); + }); + }, + ), + ListTile( + leading: const Icon(Icons.date_range_outlined), + title: const Text("Start date"), + onTap: () {}, + ), + ListTile( + leading: const Text("Checklist"), + trailing: IconButton( + onPressed: () { + setState(() { + deleteChecklist(args.crd); + }); + }, + icon: const Icon(Icons.delete)), + ), + FutureBuilder( + future: getChecklists(args.crd), + builder: ((context, snapshot) { + if (snapshot.hasData) { + List children = snapshot.data as List; + + if (children.isNotEmpty) { + return Column(children: buildChecklists(children)); + } + } + return const SizedBox.shrink(); + })), + Visibility( + visible: showChecklist, + child: TextField( + controller: checklistController, + ), + ), + const Text("Activity"), + Activities(args.crd) + ]), + ), + ), + //TODO: Add this back in when we get comments working properly + // bottomNavigationBar: Container( + // padding: const EdgeInsets.only(bottom: 5.0), + // color: whiteShade, + // width: MediaQuery.of(context).size.width * 0.8, + // height: 80, + // child: Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ + // const CircleAvatar(), + // SizedBox( + // width: MediaQuery.of(context).size.width * 0.7, + // child: TextField( + // decoration: InputDecoration( + // hintText: "Add comment", + // suffix: IconButton( + // onPressed: () {}, icon: const Icon(Icons.send))), + // ), + // ), + // IconButton(onPressed: () {}, icon: const Icon(Icons.attachment)) + // ]), + // ), + ); + } + + List buildChecklists(List chcklst) { + List lists = []; + + for (int i = 0; i < chcklst.length; i++) { + checked.putIfAbsent(i, () => false); + checked[i] = chcklst[i].status; + lists.add( + CheckboxListTile( + title: Text(chcklst[i].name), + value: checked[i], + onChanged: (bool? value) { + setState(() { + checked[i] = value!; + }); + chcklst[i].status = value!; + updateChecklist(chcklst[i]); + }, + ), + ); + } + + return lists; + } +} diff --git a/demos/supabase-trello/lib/features/closeboard/presentation/index.dart b/demos/supabase-trello/lib/features/closeboard/presentation/index.dart new file mode 100644 index 00000000..500b196e --- /dev/null +++ b/demos/supabase-trello/lib/features/closeboard/presentation/index.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; + +class CloseBoard extends StatefulWidget { + const CloseBoard({super.key}); + + @override + State createState() => _CloseBoardState(); +} + +class _CloseBoardState extends State { + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text("Board 1 is now closed"), + content: SizedBox( + height: 100, + child: Column( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width * 0.7, + child: ElevatedButton( + onPressed: () {}, child: const Text("Re-open")), + ), + SizedBox( + width: MediaQuery.of(context).size.width * 0.7, + child: OutlinedButton( + onPressed: () {}, + child: const Text( + "Delete", + style: TextStyle(color: dangerColor), + )), + ) + ], + ), + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/copyboard/presentation/index.dart b/demos/supabase-trello/lib/features/copyboard/presentation/index.dart new file mode 100644 index 00000000..af2c68eb --- /dev/null +++ b/demos/supabase-trello/lib/features/copyboard/presentation/index.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; + +import '../../../utils/color.dart'; +import '../../../utils/constant.dart'; + +class CopyBoard extends StatefulWidget { + const CopyBoard({super.key}); + + @override + State createState() => _CopyBoardState(); +} + +class _CopyBoardState extends State { + final TextEditingController nameController = TextEditingController(); + String? dropdownValue; + List workspaces = []; + Map? visibilityDropdownValue; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: const Icon( + Icons.close, + size: 30, + )), + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + controller: nameController, + decoration: const InputDecoration( + border: UnderlineInputBorder(), labelText: "Board name"), + ), + const Text("Workspace"), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: DropdownButton( + isExpanded: true, + value: dropdownValue, + icon: const Icon(Icons.keyboard_arrow_down), + elevation: 16, + style: const TextStyle(color: brandColor), + underline: Container( + height: 2, + color: brandColor, + ), + onChanged: (String? value) { + // This is called when the user selects an item. + setState(() { + dropdownValue = value!; + }); + }, + items: + workspaces.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + ), + const Text("Visibility"), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: DropdownButton>( + hint: const Text("Visibility"), + isExpanded: true, + value: visibilityDropdownValue, + icon: const Icon(Icons.keyboard_arrow_down), + elevation: 16, + style: const TextStyle(color: brandColor), + underline: Container( + height: 2, + color: brandColor, + ), + onChanged: (Map? value) { + setState(() { + visibilityDropdownValue = value!; + }); + }, + items: visibilityConfigurations + .map>>( + (Map value) { + return DropdownMenuItem>( + value: value, + child: Text(value["type"]!), + ); + }).toList(), + ), + ), + SwitchListTile( + value: false, + onChanged: ((value) {}), + title: const Text("Keep cards"), + ), + const Text( + "Activities and members will not be copied to the new board", + style: TextStyle(fontSize: 12), + ) + ], + ), + ), + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/createboard/presentation/index.dart b/demos/supabase-trello/lib/features/createboard/presentation/index.dart new file mode 100644 index 00000000..ecc3b997 --- /dev/null +++ b/demos/supabase-trello/lib/features/createboard/presentation/index.dart @@ -0,0 +1,145 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/main.dart'; +import 'package:trelloappclone_flutter/models/board.dart'; +import 'package:trelloappclone_flutter/models/workspace.dart'; + +import '../../../utils/color.dart'; +import '../../../utils/constant.dart'; +import '../../../utils/service.dart'; + +class CreateBoard extends StatefulWidget { + const CreateBoard({super.key}); + + @override + State createState() => _CreateBoardState(); +} + +class _CreateBoardState extends State with Service { + final TextEditingController nameController = TextEditingController(); + Workspace? dropdownValue; + List workspaces = []; + Map? visibilityDropdownValue; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: const Icon(Icons.close)), + title: const Text("Create board"), + centerTitle: false, + ), + body: Padding( + padding: const EdgeInsets.all(30.0), + child: Column( + children: [ + TextField( + controller: nameController, + decoration: const InputDecoration(hintText: "Enter Board name"), + ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: DropdownButton( + hint: const Text("Workspace"), + isExpanded: true, + value: dropdownValue, + icon: const Icon(Icons.keyboard_arrow_down), + elevation: 16, + style: const TextStyle(color: brandColor), + underline: Container( + height: 2, + color: brandColor, + ), + onChanged: (Workspace? value) { + setState(() { + dropdownValue = value!; + }); + }, + items: trello.workspaces + .map>((Workspace value) { + return DropdownMenuItem( + value: value, + child: Text(value.name), + ); + }).toList(), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: DropdownButton>( + hint: const Text("Visibility"), + isExpanded: true, + value: visibilityDropdownValue, + icon: const Icon(Icons.keyboard_arrow_down), + elevation: 16, + style: const TextStyle(color: brandColor), + underline: Container( + height: 2, + color: brandColor, + ), + onChanged: (Map? value) { + setState(() { + visibilityDropdownValue = value!; + }); + }, + items: visibilityConfigurations + .map>>( + (Map value) { + return DropdownMenuItem>( + value: value, + child: Text(value["type"]!), + ); + }).toList(), + ), + ), + Row( + children: [ + const Text("Board backgroud"), + const Spacer(), + GestureDetector( + onTap: () { + Navigator.pushNamed(context, '/boardbackground') + .then((_) => setState(() {})); + }, + child: Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: Color(int.parse( + trello.selectedBackground.substring(1, 7), + radix: 16) + + 0xFF000000))), + ), + ], + ), + Align( + alignment: Alignment.center, + child: Container( + padding: const EdgeInsets.only(top: 10), + width: MediaQuery.of(context).size.width * 0.8, + height: 60, + child: ElevatedButton( + onPressed: () { + createBoard( + context, + Board( + id: randomUuid(), + workspaceId: dropdownValue!.id, + userId: trello.user.id, + name: nameController.text, + visibility: visibilityDropdownValue!["type"]!, + background: trello.selectedBackground)); + }, + child: const Text("Create board"), + ), + ), + ) + ], + ), + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/createcard/presentation/index.dart b/demos/supabase-trello/lib/features/createcard/presentation/index.dart new file mode 100644 index 00000000..8b730685 --- /dev/null +++ b/demos/supabase-trello/lib/features/createcard/presentation/index.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; + +import '../../../utils/color.dart'; + +class CreateCard extends StatefulWidget { + const CreateCard({super.key}); + + @override + State createState() => _CreateCardState(); +} + +class _CreateCardState extends State { + String? dropdownValue; + List boards = ["Board 1"]; + String? listdropdownvalue; + List lists = ["List 1"]; + final TextEditingController nameController = TextEditingController(); + final TextEditingController descriptionController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + onPressed: () { + Navigator.pushNamed(context, '/home'); + }, + icon: const Icon(Icons.close), + ), + title: const Text("New card"), + centerTitle: false, + actions: [IconButton(onPressed: () {}, icon: const Icon(Icons.check))], + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Board"), + DropdownButton( + isExpanded: true, + value: dropdownValue, + icon: const Icon(Icons.keyboard_arrow_down), + elevation: 16, + style: const TextStyle(color: brandColor), + underline: Container( + height: 2, + color: brandColor, + ), + onChanged: (String? value) { + // This is called when the user selects an item. + setState(() { + dropdownValue = value!; + }); + }, + items: boards.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + const Text("List"), + DropdownButton( + isExpanded: true, + value: dropdownValue, + icon: const Icon(Icons.keyboard_arrow_down), + elevation: 16, + style: const TextStyle(color: brandColor), + underline: Container( + height: 2, + color: brandColor, + ), + onChanged: (String? value) { + // This is called when the user selects an item. + setState(() { + listdropdownvalue = value!; + }); + }, + items: lists.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + Container( + color: brandColor, + margin: const EdgeInsets.all(10.0), + child: Card( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: nameController, + decoration: + const InputDecoration(hintText: "Card name"), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: descriptionController, + decoration: + const InputDecoration(hintText: "Card description"), + ), + ), + const ListTile( + leading: Icon(Icons.person_add), + title: Text("Jane Doe"), + ), + ListTile( + leading: const Icon(Icons.lock_clock), + title: const Text("Start date..."), + onTap: () {}, + ), + ListTile( + title: const Text("Due date..."), + onTap: () {}, + ), + ListTile( + leading: const Icon(Icons.attachment), + title: const Text("Attachment"), + onTap: () {}, + ), + ], + ), + ), + ) + ], + ), + )), + ); + } +} diff --git a/demos/supabase-trello/lib/features/createworkspace/presentation/index.dart b/demos/supabase-trello/lib/features/createworkspace/presentation/index.dart new file mode 100644 index 00000000..05691cf4 --- /dev/null +++ b/demos/supabase-trello/lib/features/createworkspace/presentation/index.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/constant.dart'; + +import '../../../utils/color.dart'; +import '../../../utils/service.dart'; + +class CreateWorkspace extends StatefulWidget { + const CreateWorkspace({super.key}); + + @override + State createState() => _CreateWorkspaceState(); +} + +class _CreateWorkspaceState extends State with Service { + final TextEditingController nameController = TextEditingController(); + final TextEditingController descriptionController = TextEditingController(); + Map? dropdownValue; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Create Workspace"), + centerTitle: false, + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.only(bottom: 8.0), + child: Text( + "Let's build a Workspace", + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + ), + ), + const Text( + "Boost your productivity by making it easier for everyone to access boards in one location", + style: TextStyle(fontSize: 16), + ), + TextField( + controller: nameController, + decoration: + const InputDecoration(hintText: "Enter workspace name"), + ), + const Padding( + padding: EdgeInsets.only(top: 8.0), + child: Text("Visibility"), + ), + DropdownButton>( + isExpanded: true, + value: dropdownValue, + icon: const Icon(Icons.keyboard_arrow_down), + elevation: 16, + style: const TextStyle(color: brandColor), + underline: Container( + height: 2, + color: brandColor, + ), + onChanged: (Map? value) { + // This is called when the user selects an item. + setState(() { + dropdownValue = value!; + }); + }, + items: visibilityConfigurations + .map>>( + (Map value) { + return DropdownMenuItem>( + value: value, + child: Text(value["type"]!), + ); + }).toList(), + ), + const Padding( + padding: EdgeInsets.only(top: 10.0), + child: Text("Description"), + ), + TextField( + controller: descriptionController, + maxLines: null, + minLines: 4, + ), + Align( + alignment: Alignment.center, + child: Container( + padding: const EdgeInsets.only(top: 10), + width: MediaQuery.of(context).size.width * 0.8, + height: 60, + child: ElevatedButton( + onPressed: () { + createWorkspace(context, + name: nameController.text, + description: descriptionController.text, + visibility: dropdownValue!["type"] ?? ""); + }, + child: const Text("Create"))), + ) + ], + ), + ), + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/drawer/presentation/index.dart b/demos/supabase-trello/lib/features/drawer/presentation/index.dart new file mode 100644 index 00000000..3d914278 --- /dev/null +++ b/demos/supabase-trello/lib/features/drawer/presentation/index.dart @@ -0,0 +1,135 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/features/workspace/domain/workspace_arguments.dart'; +import 'package:trelloappclone_flutter/main.dart'; +import 'package:trelloappclone_flutter/models/workspace.dart'; + +import '../../../utils/color.dart'; +import '../../../utils/service.dart'; +import '../../workspace/presentation/index.dart'; + +class CustomDrawer extends StatefulWidget { + const CustomDrawer({super.key}); + + @override + State createState() => _CustomDrawerState(); +} + +class _CustomDrawerState extends State with Service { + bool active = true; + @override + Widget build(BuildContext context) { + return Drawer( + child: ListView(children: [ + DrawerHeader( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const CircleAvatar( + backgroundColor: brandColor, + ), + Padding( + padding: const EdgeInsets.only(top: 5.0), + child: Text(trello.user.name ?? trello.user.email), + ), + Text("@${trello.user.name!.toLowerCase().replaceAll(" ", "")}"), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(trello.user.email), + IconButton( + onPressed: () { + setState(() { + active = !active; + }); + }, + icon: Icon((active) + ? Icons.keyboard_arrow_down + : Icons.keyboard_arrow_up)) + ], + ) + ], + )), + (active) + ? Column( + children: [ + ListTile( + leading: const Icon( + Icons.pages, + color: brandColor, + ), + title: const Text( + 'Boards', + style: TextStyle( + color: brandColor, fontWeight: FontWeight.bold), + ), + onTap: () { + Navigator.pushNamed(context, '/home'); + }, + ), + const Divider( + height: 2, + thickness: 2, + color: brandColor, + ), + // TODO: Show Cards assigned to logged in user + // ListTile( + // leading: const Icon(Icons.card_membership), + // title: const Text("My cards"), + // onTap: () { + // Navigator.pushNamed(context, '/mycards'); + // }, + // ), + ListTile( + leading: const Icon(Icons.settings), + enabled: false, + title: const Text("Settings"), + onTap: () { + Navigator.pushNamed(context, '/settings'); + }, + ), + ListTile( + leading: const Icon(Icons.help_outline_rounded), + enabled: false, + title: const Text("Help!"), + onTap: () {}, + ), + ListTile( + leading: const Icon(Icons.logout), + title: const Text("Log Out"), + onTap: () { + logOut(context); + Navigator.pushNamed(context, '/'); + }, + ), + ], + ) + : ListTile( + leading: const Icon(Icons.add), + title: const Text('Add account'), + onTap: () {}, + ) + ]), + ); + } + + List buildWorkspaces(List wkspcs) { + List tiles = []; + for (int i = 0; i < wkspcs.length; i++) { + tiles.add(ListTile( + leading: const Icon(Icons.people), + title: Text(wkspcs[i].name), + trailing: IconButton( + icon: const Icon(Icons.more_horiz), + onPressed: () { + Navigator.pushNamed(context, '/workspacemenu'); + }, + ), + onTap: () { + Navigator.pushNamed(context, WorkspaceScreen.routeName, + arguments: WorkspaceArguments(wkspcs[i])); + }, + )); + } + return tiles; + } +} diff --git a/demos/supabase-trello/lib/features/editlabels/presentation/index.dart b/demos/supabase-trello/lib/features/editlabels/presentation/index.dart new file mode 100644 index 00000000..caa1fa9e --- /dev/null +++ b/demos/supabase-trello/lib/features/editlabels/presentation/index.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/main.dart'; +import 'package:trelloappclone_flutter/models/card_label.dart'; +import '../../../utils/service.dart'; + +class EditLabels extends StatefulWidget { + final String cardId; + + const EditLabels({Key? key, required this.cardId}) : super(key: key); + + @override + State createState() => _EditLabelsState(); +} + +class _EditLabelsState extends State with Service { + late List switchStates; // List to track the state of each switch + + @override + void initState() { + super.initState(); + // Initialize the switchStates list with default values (e.g., all false) + switchStates = + List.filled(trello.selectedBoard.boardLabels!.length, false); + // Set the switchStates list to true for each label that is already on the card + for (int i = 0; i < trello.selectedBoard.boardLabels!.length; i++) { + for (int j = 0; j < trello.selectedCard!.cardLabels!.length; j++) { + if (trello.selectedBoard.boardLabels![i].id == + trello.selectedCard!.cardLabels![j].boardLabelId) { + switchStates[i] = true; + } + } + } + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text("Edit labels"), + content: SizedBox( + height: 200, + child: Column(children: buildWidget()), + ), + ); + } + + List buildWidget() { + // Create and initialize a list of TextEditingControllers + List controllers = trello.selectedBoard.boardLabels! + .map((label) => TextEditingController(text: label.title)) + .toList(); + + List labelContainers = []; + for (int i = 0; i < trello.selectedBoard.boardLabels!.length; i++) { + labelContainers.add(Padding( + padding: const EdgeInsets.only(bottom: 5.0), + child: Container( + height: 35, + decoration: BoxDecoration( + color: Color(int.parse(trello.selectedBoard.boardLabels![i].color, + radix: 16) + + 0xFF000000), + borderRadius: BorderRadius.circular(5), + ), + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: TextField( + controller: controllers[i], + onChanged: (value) { + // Update the label title in the database + trello.selectedBoard.boardLabels![i].title = value; + updateBoardLabel(trello.selectedBoard.boardLabels![i]); + }, + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(5.0), + borderSide: BorderSide.none, + ), + filled: false, + contentPadding: + const EdgeInsets.symmetric(horizontal: 10), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Align( + alignment: Alignment.center, + child: Transform.scale( + scale: 0.75, // Adjust the scale to make the switch smaller + child: Switch( + value: switchStates[ + i], // You might want to manage this state properly + onChanged: (bool value) async { + // Handle toggle logic + if (value) { + // Add label to card via service.data + var cardLabel = await addCardLabel( + CardLabel( + id: randomUuid(), + workspaceId: trello.selectedBoard + .boardLabels![i].workspaceId, + boardLabelId: + trello.selectedBoard.boardLabels![i].id, + boardId: trello + .selectedBoard.boardLabels![i].boardId, + cardId: widget.cardId, + dateCreated: DateTime.now()), + trello.selectedBoard.boardLabels![i]); + trello.selectedCard!.cardLabels!.add(cardLabel); + } else { + // Remove label from card + deleteCardLabel(widget.cardId, + trello.selectedBoard.boardLabels![i]); + trello.selectedCard!.cardLabels!.removeWhere( + (element) => + element.boardLabelId == + trello.selectedBoard.boardLabels![i].id); + } + setState(() { + switchStates[i] = + value; // Update the state when the switch is toggled + }); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + ), + ), + ], + ), + ), + )); + } + return labelContainers; + } +} diff --git a/demos/supabase-trello/lib/features/emailtoboard/presentation/index.dart b/demos/supabase-trello/lib/features/emailtoboard/presentation/index.dart new file mode 100644 index 00000000..6e2e4225 --- /dev/null +++ b/demos/supabase-trello/lib/features/emailtoboard/presentation/index.dart @@ -0,0 +1,135 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; + +class EmailToBoard extends StatefulWidget { + const EmailToBoard({super.key}); + + @override + State createState() => _EmailToBoardState(); +} + +class _EmailToBoardState extends State { + final TextEditingController emailController = TextEditingController(); + String? dropdownValue; + String? dropdownPosition; + List list = ["To Do"]; + List position = ["Bottom"]; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Email-to-board settings"), + centerTitle: false, + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Your email address for this board"), + TextField( + controller: emailController, + ), + ListTile( + leading: const Icon(Icons.copy), + title: const Text("Copy this address"), + onTap: () {}, + ), + ListTile( + leading: const Icon(Icons.email_outlined), + title: const Text("Email me this address"), + onTap: () {}, + ), + ListTile( + leading: const Icon(Icons.mark_email_read_outlined), + title: const Text("Generate a new email address"), + onTap: () {}, + ), + const Divider( + height: 2, + thickness: 1, + ), + const Padding( + padding: EdgeInsets.only(bottom: 8.0, top: 8.0), + child: Text( + "Your emailed cards appear in...", + style: TextStyle(fontWeight: FontWeight.w600), + ), + ), + const Text( + "List", + style: + TextStyle(color: brandColor, fontWeight: FontWeight.bold), + ), + DropdownButton( + isExpanded: true, + value: dropdownValue, + icon: const Icon(Icons.keyboard_arrow_down), + elevation: 16, + style: const TextStyle(color: themeColor), + underline: Container( + height: 2, + color: brandColor, + ), + onChanged: (String? value) { + setState(() { + dropdownValue = value!; + }); + }, + items: list.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + const Text( + "Position", + style: + TextStyle(color: brandColor, fontWeight: FontWeight.bold), + ), + DropdownButton( + isExpanded: true, + value: dropdownPosition, + icon: const Icon(Icons.keyboard_arrow_down), + elevation: 16, + style: const TextStyle(color: themeColor), + underline: Container( + height: 2, + color: brandColor, + ), + onChanged: (String? value) { + setState(() { + dropdownPosition = value!; + }); + }, + items: position.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + const Padding( + padding: EdgeInsets.only(top: 18.0), + child: Divider( + height: 2, + thickness: 1, + ), + ), + const Padding( + padding: EdgeInsets.only(top: 18.0), + child: Text( + "Tip: Don't share this email address. Anyone who has it can add cards as you. When composing emails , the card title goes in the subject and the card description in the body", + style: TextStyle(fontWeight: FontWeight.bold), + ), + ) + ], + ), + ), + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/emptywidget/index.dart b/demos/supabase-trello/lib/features/emptywidget/index.dart new file mode 100644 index 00000000..bb7739ad --- /dev/null +++ b/demos/supabase-trello/lib/features/emptywidget/index.dart @@ -0,0 +1,385 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; + +/// {@tool snippet} +/// +/// This example shows how to use [EmptyWidget] +/// +/// ``` dart +/// EmptyWidget( +/// image: null, +/// packageImage: PackageImage.Image_1, +/// title: 'No Notification', +/// subTitle: 'No notification available yet', +/// titleTextStyle: TextStyle( +/// fontSize: 22, +/// color: Color(0xff9da9c7), +/// fontWeight: FontWeight.w500, +/// ), +/// subtitleTextStyle: TextStyle( +/// fontSize: 14, +/// color: Color(0xffabb8d6), +/// ), +/// ) +/// ``` +/// {@end-tool} + +class EmptyWidget extends StatefulWidget { + EmptyWidget({ + this.title, + this.subTitle, + this.image, + this.subtitleTextStyle, + this.titleTextStyle, + this.packageImage, + this.hideBackgroundAnimation = false, + }); + + /// Display images from project assets + final String? image; /*!*/ + + /// Display image from package assets + final PackageImage? packageImage; /*!*/ + + /// Set text for subTitle + final String? subTitle; /*!*/ + + /// Set text style for subTitle + final TextStyle? subtitleTextStyle; /*!*/ + + /// Set text for title + final String? title; /*!*/ + + /// Text style for title + final TextStyle? titleTextStyle; /*!*/ + + /// Hides the background circular ball animation + /// + /// By default `false` value is set + final bool? hideBackgroundAnimation; + + @override + State createState() => _EmptyListWidgetState(); +} + +class _EmptyListWidgetState extends State + with TickerProviderStateMixin { + // String title, subTitle,image = 'assets/images/emptyImage.png'; + + late AnimationController _backgroundController; + + late Animation _imageAnimation; /*!*/ + AnimationController? _imageController; /*!*/ + late PackageImage? _packageImage; /*!*/ + TextStyle? _subtitleTextStyle; /*!*/ + TextStyle? _titleTextStyle; /*!*/ + late AnimationController _widgetController; /*!*/ + + @override + void dispose() { + _backgroundController.dispose(); + _imageController!.dispose(); + _widgetController.dispose(); + super.dispose(); + } + + @override + void initState() { + _backgroundController = AnimationController( + duration: const Duration(minutes: 1), + vsync: this, + lowerBound: 0, + upperBound: 20) + ..repeat(); + _widgetController = AnimationController( + duration: const Duration(seconds: 1), + vsync: this, + lowerBound: 0, + upperBound: 1) + ..forward(); + _imageController = AnimationController( + duration: const Duration(seconds: 4), + vsync: this, + )..repeat(); + _imageAnimation = Tween(begin: 0, end: 10).animate( + CurvedAnimation(parent: _imageController!, curve: Curves.linear), + ); + super.initState(); + } + + animationListner() { + if (_imageController == null) { + return; + } + if (_imageController!.isCompleted) { + setState(() { + _imageController!.reverse(); + }); + } else { + setState(() { + _imageController!.forward(); + }); + } + } + + Widget _imageWidget() { + bool isPackageImage = _packageImage != null; + return Expanded( + flex: 3, + child: AnimatedBuilder( + animation: _imageAnimation, + builder: (BuildContext context, Widget? child) { + return Transform.translate( + offset: Offset( + 0, + sin(_imageAnimation.value > .9 + ? 1 - _imageAnimation.value + : _imageAnimation.value)), + child: child, + ); + }, + child: Padding( + padding: EdgeInsets.all(10), + child: Image.asset( + isPackageImage ? _packageImage.encode()! : widget.image!, + fit: BoxFit.contain, + package: isPackageImage ? 'empty_widget' : null, + ), + ), + ), + ); + } + + Widget _imageBackground() { + return Container( + width: EmptyWidgetUtility.getHeightDimention( + context, EmptyWidgetUtility.fullWidth(context) * .95), + height: EmptyWidgetUtility.getHeightDimention( + context, EmptyWidgetUtility.fullWidth(context) * .95), + decoration: BoxDecoration(boxShadow: [ + BoxShadow( + offset: Offset(0, 0), + color: Color(0xffe2e5ed), + ), + BoxShadow( + blurRadius: 30, + offset: Offset(20, 0), + color: Color(0xffffffff), + spreadRadius: -5), + ], shape: BoxShape.circle), + ); + } + + Widget _shell({Widget? child}) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + if (constraints.maxHeight > constraints.maxWidth) { + return Container( + height: constraints.maxWidth, + width: constraints.maxWidth, + child: child, + ); + } else { + return child!; + } + }); + } + + Widget _shellChild() { + _titleTextStyle = widget.titleTextStyle ?? + Theme.of(context) + .typography + .dense + .headlineSmall! + .copyWith(color: Color(0xff9da9c7)); + _subtitleTextStyle = widget.subtitleTextStyle ?? + Theme.of(context) + .typography + .dense + .bodyMedium! + .copyWith(color: Color(0xffabb8d6)); + _packageImage = widget.packageImage; + + bool anyImageProvided = widget.image == null && _packageImage == null; + + return FadeTransition( + opacity: _widgetController, + child: Container( + alignment: Alignment.center, + color: Colors.transparent, + child: Stack( + alignment: Alignment.center, + children: [ + if (!widget.hideBackgroundAnimation!) + RotationTransition( + child: _imageBackground(), + turns: _backgroundController, + ), + LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Container( + height: constraints.maxWidth, + width: constraints.maxWidth - 30, + alignment: Alignment.center, + padding: EdgeInsets.all(10), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + anyImageProvided + ? SizedBox() + : Expanded( + flex: 1, + child: Container(), + ), + anyImageProvided ? SizedBox() : _imageWidget(), + Column( + children: [ + CustomText( + msg: widget.title, + style: _titleTextStyle, + context: context, + overflow: TextOverflow.clip, + textAlign: TextAlign.center, + ), + SizedBox( + height: 10, + ), + CustomText( + msg: widget.subTitle, + style: _subtitleTextStyle, + context: context, + overflow: TextOverflow.clip, + textAlign: TextAlign.center) + ], + ), + anyImageProvided + ? SizedBox() + : Expanded( + flex: 1, + child: Container(), + ) + ], + ), + ); + }), + ], + )), + ); + } + + @override + Widget build(BuildContext context) { + return _shell(child: _shellChild()); + } +} + +// nodoc +enum PackageImage { + Image_1, + Image_2, + Image_3, + Image_4, +} + +const _$PackageImageTypeMap = { + PackageImage.Image_1: 'assets/images/emptyImage.png', + PackageImage.Image_2: 'assets/images/im_emptyIcon_1.png', + PackageImage.Image_3: 'assets/images/im_emptyIcon_2.png', + PackageImage.Image_4: 'assets/images/im_emptyIcon_3.png', +}; + +extension convert on PackageImage? { + String? encode() => _$PackageImageTypeMap[this!]; + + PackageImage? key(String value) => decodePackageImage(value); + + PackageImage? decodePackageImage(String value) { + return _$PackageImageTypeMap.entries + .singleWhere((element) => element.value == value) + .key; + } +} + +class EmptyWidgetUtility { + static double getHeightDimention(BuildContext context, double unit) { + if (fullHeight(context) <= 460.0) { + return unit / 1.5; + } else { + return getDimention(context, unit); + } + } + + static double fullHeight(BuildContext context) { + return MediaQuery.of(context).size.height; + } + + static double getDimention(context, double unit) { + if (fullWidth(context) <= 360.0) { + return unit / 1.3; + } else { + return unit; + } + } + + static double fullWidth(BuildContext context) { + return MediaQuery.of(context).size.width; + } +} + +class CustomText extends StatefulWidget { + const CustomText( + {Key? key, + this.msg, + this.style, + this.textAlign, + this.overflow, + this.context, + this.softwrap}) + : super(key: key); + + final BuildContext? context; + final String? msg; + final TextOverflow? overflow; + final bool? softwrap; + final TextStyle? style; + final TextAlign? textAlign; + + _CustomTextState createState() => _CustomTextState(); +} + +class _CustomTextState extends State { + TextStyle? style; + + @override + @override + void initState() { + style = widget.style; + super.initState(); + } + + Widget customText() { + if (widget.msg == null) { + return Container(); + } + if (widget.context != null && widget.style != null) { + var font = widget.style!.fontSize == null + ? Theme.of(context).textTheme.bodyMedium!.fontSize! + : widget.style!.fontSize!; + style = widget.style!.copyWith( + fontSize: + font - (EmptyWidgetUtility.fullWidth(context) <= 375 ? 2 : 0)); + } + return Text( + widget.msg!, + style: widget.style, + textAlign: widget.textAlign, + overflow: widget.overflow, + ); + } + + @override + Widget build(BuildContext context) { + return customText(); + } +} diff --git a/demos/supabase-trello/lib/features/generateworkspace/presentation/index.dart b/demos/supabase-trello/lib/features/generateworkspace/presentation/index.dart new file mode 100644 index 00000000..34ed35fb --- /dev/null +++ b/demos/supabase-trello/lib/features/generateworkspace/presentation/index.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/data_generator.dart'; + +import '../../../main.dart'; +import '../../../utils/service.dart'; + +class GenerateWorkspace extends StatefulWidget { + const GenerateWorkspace({super.key}); + + @override + State createState() => _GenerateWorkspaceState(); +} + +class _GenerateWorkspaceState extends State with Service { + final TextEditingController nameController = TextEditingController(); + Map? dropdownValue; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Generate Sample Workspace"), + centerTitle: false, + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.only(bottom: 8.0), + child: Text( + "This will create a Workspace with sample data", + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + ), + ), + TextField( + controller: nameController, + decoration: + const InputDecoration(hintText: "Enter workspace name"), + ), + // const Padding( + // padding: EdgeInsets.only(top: 8.0), + // child: Text("Visibility"), + // ), + // DropdownButton>( + // isExpanded: true, + // value: dropdownValue, + // icon: const Icon(Icons.keyboard_arrow_down), + // elevation: 16, + // style: const TextStyle(color: brandColor), + // underline: Container( + // height: 2, + // color: brandColor, + // ), + // onChanged: (Map? value) { + // // This is called when the user selects an item. + // setState(() { + // dropdownValue = value!; + // }); + // }, + // items: visibilityConfigurations + // .map>>( + // (Map value) { + // return DropdownMenuItem>( + // value: value, + // child: Text(value["type"]!), + // ); + // }).toList(), + // ), + // const Padding( + // padding: EdgeInsets.only(top: 10.0), + // child: Text("Description"), + // ), + // TextField( + // controller: descriptionController, + // maxLines: null, + // minLines: 4, + // ), + Align( + alignment: Alignment.center, + child: Container( + padding: const EdgeInsets.only(top: 10), + width: MediaQuery.of(context).size.width * 0.8, + height: 60, + child: ElevatedButton( + onPressed: () { + DataGenerator().createSampleWorkspace( + nameController.text, + trello, + context + ); + }, + child: const Text("Create"))), + ) + ], + ), + ), + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/home/presentation/custom_floating_action.dart b/demos/supabase-trello/lib/features/home/presentation/custom_floating_action.dart new file mode 100644 index 00000000..e9532adf --- /dev/null +++ b/demos/supabase-trello/lib/features/home/presentation/custom_floating_action.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +class CustomFloatingAction extends StatefulWidget { + final String title; + final IconData icon; + final String route; + const CustomFloatingAction(this.title, this.icon, this.route, {super.key}); + + @override + State createState() => _CustomFloatingActionState(); +} + +class _CustomFloatingActionState extends State { + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + Navigator.pushNamed(context, widget.route); + }, + child: Text.rich(TextSpan(children: [ + WidgetSpan( + child: SizedBox( + width: 150, + height: 30, + child: Card( + child: Center( + child: Text(widget.title), + )), + )), + const WidgetSpan( + child: SizedBox( + width: 20, + )), + WidgetSpan( + child: CircleAvatar( + backgroundColor: Colors.green[400], + child: Icon(widget.icon, color: Colors.white, size: 26), + )) + ])), + ); + } +} diff --git a/demos/supabase-trello/lib/features/home/presentation/custom_search.dart b/demos/supabase-trello/lib/features/home/presentation/custom_search.dart new file mode 100644 index 00000000..ed0394cc --- /dev/null +++ b/demos/supabase-trello/lib/features/home/presentation/custom_search.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/models/board.dart'; + +class CustomSearchDelegate extends SearchDelegate { + List searchTerms = []; + CustomSearchDelegate(List s) { + searchTerms = s; + } + + @override + List? buildActions(BuildContext context) { + return [ + IconButton( + onPressed: () { + query = ''; + }, + icon: const Icon(Icons.clear), + ), + ]; + } + + @override + Widget? buildLeading(BuildContext context) { + return IconButton( + onPressed: () { + close(context, null); + }, + icon: const Icon(Icons.arrow_back), + ); + } + + @override + Widget buildResults(BuildContext context) { + List matchQuery = []; + for (var brd in searchTerms) { + if (brd.name.toLowerCase().contains(query.toLowerCase())) { + matchQuery.add(brd); + } + } + return ListView.builder( + itemCount: matchQuery.length, + itemBuilder: (context, index) { + var result = matchQuery[index]; + + return ListTile( + onTap: () async { + if (context.mounted) { + Navigator.pushNamed(context, "/board"); + } + }, + title: Text(result.name), + ); + }, + ); + } + + @override + Widget buildSuggestions(BuildContext context) { + List matchQuery = []; + for (var brd in searchTerms) { + if (brd.name.toLowerCase().contains(query.toLowerCase())) { + matchQuery.add(brd); + } + } + return ListView.builder( + itemCount: matchQuery.length, + itemBuilder: (context, index) { + var result = matchQuery[index]; + + return ListTile( + onTap: () async { + if (context.mounted) { + Navigator.pushNamed(context, "/board"); + } + }, + title: Text(result.name), + ); + }, + ); + } +} diff --git a/demos/supabase-trello/lib/features/home/presentation/index.dart b/demos/supabase-trello/lib/features/home/presentation/index.dart new file mode 100644 index 00000000..ab6f6135 --- /dev/null +++ b/demos/supabase-trello/lib/features/home/presentation/index.dart @@ -0,0 +1,187 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_expandable_fab/flutter_expandable_fab.dart'; +import 'package:logging/logging.dart'; +import 'package:powersync/powersync.dart' as PowerSync; +import 'package:trelloappclone_flutter/features/emptywidget/index.dart'; +import 'package:trelloappclone_flutter/main.dart'; +import 'package:trelloappclone_flutter/models/board.dart'; +import 'package:trelloappclone_flutter/models/workspace.dart'; +import 'package:trelloappclone_flutter/features/board/domain/board_arguments.dart'; +import 'package:trelloappclone_flutter/features/board/presentation/index.dart'; +import 'package:trelloappclone_flutter/protocol/data_client.dart'; + +import '../../../utils/color.dart'; +import '../../../utils/service.dart'; +import '../../../utils/widgets.dart'; +import '../../drawer/presentation/index.dart'; +import 'custom_floating_action.dart'; + +final log = Logger('powersync-supabase'); + +class Home extends StatefulWidget { + const Home({super.key}); + + @override + State createState() => _HomeState(); +} + +class _HomeState extends State with Service { + late PowerSync.SyncStatus _connectionState; + StreamSubscription? _syncStatusSubscription; + + @override + void initState() { + super.initState(); + + _connectionState = dataClient.getCurrentSyncStatus(); + _syncStatusSubscription = dataClient.getStatusStream().listen((event) { + log.info('Sync Status: $event'); + setState(() { + _connectionState = event; + }); + }); + } + + @override + void dispose() { + super.dispose(); + _syncStatusSubscription?.cancel(); + } + + @override + Widget build(BuildContext context) { + IconButton connectedIcon = IconButton( + icon: const Icon(Icons.wifi), + tooltip: 'Connected', + onPressed: () { + switchToOfflineMode(); + }, + ); + IconButton disconnectedIcon = IconButton( + icon: const Icon(Icons.wifi_off), + tooltip: 'Not connected', + onPressed: () { + switchToOnlineMode(); + }, + ); + + return Scaffold( + appBar: AppBar( + title: const Text("Boards"), + actions: [ + IconButton( + onPressed: () { + search(context); + }, + icon: const Icon(Icons.search)), + _connectionState.connected ? connectedIcon : disconnectedIcon + ], + ), + drawer: const CustomDrawer(), + body: StreamBuilder( + stream: getWorkspacesStream(), + builder: + (BuildContext context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + List children = snapshot.data as List; + + if (children.isNotEmpty) { + return SingleChildScrollView( + child: + Column(children: buildWorkspacesAndBoards(children))); + } + } + return Center( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: EmptyWidget( + image: null, + packageImage: PackageImage.Image_1, + title: 'No Boards', + subTitle: 'Create your first Trello board', + titleTextStyle: const TextStyle( + fontSize: 22, + color: Color(0xff9da9c7), + fontWeight: FontWeight.w500, + ), + subtitleTextStyle: const TextStyle( + fontSize: 14, + color: Color(0xffabb8d6), + ), + ), + ), + ); + }), + floatingActionButtonLocation: ExpandableFab.location, + floatingActionButton: ExpandableFab( + openButtonBuilder: RotateFloatingActionButtonBuilder( + child: const Icon(Icons.add), + fabSize: ExpandableFabSize.regular, + backgroundColor: Colors.green[400], + shape: const CircleBorder(), + ), + type: ExpandableFabType.up, + children: const [ + CustomFloatingAction("Workspace", Icons.book, '/createworkspace'), + CustomFloatingAction("Board", Icons.book, '/createboard'), + CustomFloatingAction("Sample Workspace", Icons.dataset_outlined, + '/generateworkspace'), + //CustomFloatingAction("Card", Icons.card_membership, '/createcard') + ]), + ); + } + + List buildWorkspacesAndBoards(List wkspcs) { + List workspacesandboards = []; + Widget workspace; + + for (int i = 0; i < wkspcs.length; i++) { + workspace = ListTile( + tileColor: whiteShade, + leading: Text(wkspcs[i].name), + trailing: IconButton( + onPressed: () { + Navigator.pushNamed(context, '/workspacemenu'); + }, + icon: const Icon(Icons.more_horiz)), + ); + + workspacesandboards.add(workspace); + + workspacesandboards.add(StreamBuilder( + stream: getBoardsStream(wkspcs[i].id), + builder: (BuildContext context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + List children = snapshot.data as List; + + if (children.isNotEmpty) { + return Column(children: buildBoards(children, wkspcs[i])); + } + } + return const SizedBox.shrink(); + }, + )); + } + return workspacesandboards; + } + + List buildBoards(List brd, Workspace wkspc) { + List boards = []; + for (int j = 0; j < brd.length; j++) { + boards.add(ListTile( + leading: ColorSquare( + bckgrd: brd[j].background, + ), + title: Text(brd[j].name), + onTap: () { + Navigator.pushNamed(context, BoardScreen.routeName, + arguments: BoardArguments(brd[j], wkspc)); + }, + )); + } + + return boards; + } +} diff --git a/demos/supabase-trello/lib/features/invitemember/presentation/index.dart b/demos/supabase-trello/lib/features/invitemember/presentation/index.dart new file mode 100644 index 00000000..bcb4ad3a --- /dev/null +++ b/demos/supabase-trello/lib/features/invitemember/presentation/index.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:status_alert/status_alert.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; +import 'package:trelloappclone_flutter/utils/service.dart'; +import 'package:trelloappclone_flutter/models/member.dart'; + +import '../../../main.dart'; + +class InviteMember extends StatefulWidget { + const InviteMember({super.key}); + + @override + State createState() => _InviteMemberState(); +} + +class _InviteMemberState extends State with Service { + final TextEditingController emailcontroller = TextEditingController(); + final List _currentMembers = []; + + @override + void initState() { + super.initState(); + _currentMembers.addAll(trello.selectedWorkspace.members ?? []); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: const Icon(Icons.close, size: 30), + ), + title: Text("Invite to ${trello.selectedWorkspace.name}"), + centerTitle: false, + // actions: [ + // IconButton(onPressed: () {}, icon: const Icon(Icons.contacts)) + // ], + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: TextField( + controller: emailcontroller, + textCapitalization: TextCapitalization.none, + keyboardType: TextInputType.emailAddress, + decoration: const InputDecoration(hintText: "Email"), + ), + ), + Card( + child: ListTile( + textColor: brandColor, + title: const Text("Add Existing User"), + subtitle: const Text("Add user with email to workspace."), + trailing: IconButton( + icon: const Icon( + Icons.add_circle_outline, + color: brandColor, + ), + onPressed: () { + inviteUserToWorkspace( + emailcontroller.text, trello.selectedWorkspace) + .then((succeeded) { + if (succeeded) { + setState(() { + _currentMembers.clear(); + _currentMembers + .addAll(trello.selectedWorkspace.members ?? []); + }); + StatusAlert.show(context, + duration: const Duration(seconds: 3), + title: 'Added Member', + subtitle: + '${emailcontroller.text} added to workspace.', + configuration: const IconConfiguration( + icon: Icons.check, color: brandColor), + maxWidth: 260); + } else { + StatusAlert.show(context, + duration: const Duration(seconds: 3), + title: 'Add Failed', + subtitle: + '${emailcontroller.text} not an existing user.', + configuration: const IconConfiguration( + icon: Icons.error_outline, color: brandColor), + maxWidth: 260); + } + }); + }, + ), + ), + ), + const Padding( + padding: EdgeInsets.only(top: 18.0, bottom: 18), + child: Align( + alignment: Alignment.topLeft, + child: Text( + "Current Board Members", + style: TextStyle(fontWeight: FontWeight.w600), + ), + ), + ), + _buildMembersList(), + // Padding( + // padding: EdgeInsets.only(bottom: 8.0), + // child: Text( + // "Work together on a board", + // style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + // ), + // ), + // Text( + // "Use the search bar or invite link to share this board with others", + // textAlign: TextAlign.center, + // ) + ], + ), + ), + ), + ); + } + + Widget _buildMembersList() { + List memberTiles = []; + for (var member in _currentMembers) { + memberTiles.add(ListTile( + leading: CircleAvatar( + backgroundColor: brandColor, + child: Text(member.name[0].toUpperCase()), + ), + title: Text(member.name), + trailing: const Text("Admin"), + )); + } + return Column( + children: memberTiles, + ); + } +} diff --git a/demos/supabase-trello/lib/features/landing/presentation/bottomsheet.dart b/demos/supabase-trello/lib/features/landing/presentation/bottomsheet.dart new file mode 100644 index 00000000..c58ed811 --- /dev/null +++ b/demos/supabase-trello/lib/features/landing/presentation/bottomsheet.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:trelloappclone_flutter/features/signtotrello/domain/sign_arguments.dart'; +import 'package:trelloappclone_flutter/features/signtotrello/presentation/index.dart'; + +import '../../../utils/color.dart'; +import '../../../utils/config.dart'; + +class LandingBottomSheet extends StatefulWidget { + final Enum type; + const LandingBottomSheet(this.type, {super.key}); + + @override + State createState() => _LandingBottomSheetState(); +} + +class _LandingBottomSheetState extends State { + @override + Widget build(BuildContext context) { + return SizedBox( + height: 250, + child: ListView( + children: [ + ListTile( + onTap: () { + Navigator.pop(context); + Navigator.pushNamed(context, SignToTrello.routeName, + arguments: SignArguments(widget.type)); + }, + leading: const Icon( + Icons.email, + color: brandColor, + ), + title: Text( + (widget.type == Sign.signUp) + ? " SIGN UP WITH EMAIL" + : "LOG IN WITH EMAIL", + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + ListTile( + onTap: null, + leading: Icon( + MdiIcons.google, + color: Colors.grey, + ), + title: Text( + (widget.type == Sign.signUp) + ? " SIGN UP WITH GOOGLE" + : "LOG IN WITH GOOGLE", + style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.grey), + ), + ), + ListTile( + onTap: null, + leading: Icon( + MdiIcons.microsoft, + color: Colors.grey, + ), + title: Text( + (widget.type == Sign.signUp) + ? " SIGN UP WITH MICROSOFT" + : "LOG IN WITH MICROSOFT", + style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.grey), + ), + ), + ListTile( + onTap: null, + leading: Icon( + MdiIcons.apple, + color: Colors.grey, + ), + title: Text( + (widget.type == Sign.signUp) + ? " SIGN UP WITH APPLE" + : "LOG IN WITH APPLE", + style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.grey), + ), + ) + ], + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/landing/presentation/index.dart b/demos/supabase-trello/lib/features/landing/presentation/index.dart new file mode 100644 index 00000000..6a5b6e6c --- /dev/null +++ b/demos/supabase-trello/lib/features/landing/presentation/index.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/features/landing/presentation/bottomsheet.dart'; + +import '../../../utils/color.dart'; +import '../../../utils/config.dart'; +import '../../../utils/constant.dart'; +import '../../../utils/service.dart'; + +class Landing extends StatefulWidget { + const Landing({super.key}); + + @override + State createState() => _LandingState(); +} + +class _LandingState extends State with Service { + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Image.asset( + logo, + width: 30, + height: 30, + ), + ), + Image.asset( + landingImage, + height: MediaQuery.of(context).size.height * 0.4, + ), + const Padding( + padding: EdgeInsets.all(25.0), + child: Text( + headline, + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + ), + Container( + margin: const EdgeInsets.all(8.0), + width: MediaQuery.of(context).size.width * 0.8, + height: 50, + child: ElevatedButton( + onPressed: () { + showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return const LandingBottomSheet(Sign.signUp); + }); + }, + child: const Text("Sign up"), + ), + ), + Container( + margin: const EdgeInsets.all(8.0), + width: MediaQuery.of(context).size.width * 0.8, + height: 50, + child: OutlinedButton( + onPressed: () { + showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return const LandingBottomSheet(Sign.logIn); + }); + }, + style: OutlinedButton.styleFrom( + side: const BorderSide(width: 1.0, color: brandColor)), + child: const Text("Log in"), + ), + ), + const Text( + terms, + textAlign: TextAlign.center, + ), + const SizedBox( + height: 10, + ), + const Text( + contact, + style: TextStyle(decoration: TextDecoration.underline), + ) + ], + )), + ); + } +} diff --git a/demos/supabase-trello/lib/features/members/presentation/index.dart b/demos/supabase-trello/lib/features/members/presentation/index.dart new file mode 100644 index 00000000..c6ae50f1 --- /dev/null +++ b/demos/supabase-trello/lib/features/members/presentation/index.dart @@ -0,0 +1,131 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; +import 'package:trelloappclone_flutter/utils/service.dart'; +import 'package:trelloappclone_flutter/models/member.dart'; + +import '../../../main.dart'; + +class Members extends StatefulWidget { + const Members({super.key}); + + @override + State createState() => _MembersState(); +} + +class _MembersState extends State with Service { + final List _currentMembers = []; + + @override + void initState() { + super.initState(); + _currentMembers.addAll(trello.selectedWorkspace.members ?? []); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Members"), + centerTitle: false, + actions: [ + TextButton( + onPressed: () { + Navigator.pushNamed(context, '/invitemember'); + }, + child: const Text( + "INVITE", + style: TextStyle(color: whiteShade), + )) + ], + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Members (${_currentMembers.length})"), + ListView( + shrinkWrap: true, + children: _buildMembersList(), + ) + ], + ), + )), + ); + } + + List _buildMembersList() { + List memberTiles = []; + for (var member in _currentMembers) { + memberTiles.add(ListTile( + leading: CircleAvatar( + backgroundColor: brandColor, + child: Text(member.name[0].toUpperCase()), + ), + title: Text(member.name), + trailing: Text( + member.role, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + onTap: () { + showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return SizedBox( + height: MediaQuery.of(context).size.height * 0.4, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile( + leading: CircleAvatar( + backgroundColor: brandColor, + child: Text(member.name[0].toUpperCase()), + ), + title: Text(member.name), + ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(member.role), + ), + const Text( + "Can view, create and edit Workspace boards, and change settings for the workspace"), + Align( + alignment: Alignment.center, + child: Container( + padding: const EdgeInsets.only(top: 8.0), + width: MediaQuery.of(context).size.width * 0.8, + height: 50, + child: ElevatedButton( + onPressed: () { + removeMemberFromWorkspace( + member, trello.selectedWorkspace) + .then((updatedWorkspace) { + setState(() { + _currentMembers.clear(); + _currentMembers.addAll( + updatedWorkspace.members ?? []); + }); + }); + Navigator.of(context).pop(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: dangerColor), + child: Text(member.userId == trello.user.id + ? "Leave workspace" + : "Remove from workspace")), + ), + ) + ]), + ), + ); + }); + }, + )); + } + + return memberTiles; + } +} diff --git a/demos/supabase-trello/lib/features/mycards/presentation/index.dart b/demos/supabase-trello/lib/features/mycards/presentation/index.dart new file mode 100644 index 00000000..25416bdc --- /dev/null +++ b/demos/supabase-trello/lib/features/mycards/presentation/index.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; + +import '../../drawer/presentation/index.dart'; + +class MyCards extends StatefulWidget { + const MyCards({super.key}); + + @override + State createState() => _MyCardsState(); +} + +class _MyCardsState extends State { + String selectedValue = "Board"; + List list = ["Board", "Date"]; + String? dropdownValue; + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Row(children: [ + Text("My cards by $selectedValue"), + SizedBox( + child: DropdownButton( + value: dropdownValue, + icon: const Icon( + Icons.keyboard_arrow_down, + color: Colors.white, + ), + underline: const SizedBox.shrink(), + elevation: 16, + onChanged: (String? value) { + setState(() { + selectedValue = value!; + }); + }, + items: list.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + )) + ]), + centerTitle: false, + actions: [IconButton(onPressed: () {}, icon: const Icon(Icons.search))], + ), + drawer: const CustomDrawer(), + body: const Center( + child: Padding( + padding: EdgeInsets.all(10.0), + child: Text( + "When you are assigned to cards they will show up here", + textAlign: TextAlign.center, + style: TextStyle(fontSize: 16), + ), + ), + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/notifications/presentation/index.dart b/demos/supabase-trello/lib/features/notifications/presentation/index.dart new file mode 100644 index 00000000..a814adea --- /dev/null +++ b/demos/supabase-trello/lib/features/notifications/presentation/index.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; + +class Notifications extends StatefulWidget { + const Notifications({super.key}); + + @override + State createState() => _NotificationsState(); +} + +class _NotificationsState extends State { + List popupmenu = ["Push notification settings"]; + late String selectedMenu; + List list = ["All Types", "Me", "Comments", "Join requests"]; + String selected = "All Types"; + bool show = true; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: const Icon( + Icons.close, + size: 30, + )), + actions: [ + IconButton( + onPressed: () {}, + icon: const Icon( + Icons.library_add_check_sharp, + size: 30, + )), + PopupMenuButton( + initialValue: popupmenu[0], + onSelected: (String item) { + setState(() { + selectedMenu = item; + }); + }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: popupmenu[0], + child: Text(popupmenu[0]), + ) + ]) + ], + ), + body: Stack(children: [ + Row( + children: [ + OutlinedButton( + onPressed: () { + showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return SizedBox( + height: 250, + child: ListView(children: buildWidgets()), + ); + }); + }, + child: Row( + children: [ + Text(selected), + const Icon(Icons.keyboard_arrow_down) + ], + )), + Visibility( + visible: show, + child: OutlinedButton( + onPressed: () { + setState(() { + show = !show; + }); + }, + child: const Text("Unread"), + )), + Visibility( + visible: !show, + child: ElevatedButton.icon( + onPressed: () { + setState(() { + show = !show; + }); + }, + icon: const Icon(Icons.check), + label: const Text("Unread"))), + ], + ), + const Center( + child: Padding( + padding: EdgeInsets.all(10.0), + child: Text( + "You don't have any notifications that match the selected filters", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), + textAlign: TextAlign.center, + ), + ), + ) + ]), + ); + } + + List buildWidgets() { + List dropdownLists = []; + for (int i = 0; i < list.length; i++) { + dropdownLists.add(ListTile( + onTap: () { + setState(() { + selected = list[i]; + }); + Navigator.pop(context); + }, + leading: + (selected == list[i]) ? const Icon(Icons.check) : const SizedBox(), + title: Text( + list[i], + style: const TextStyle(fontWeight: FontWeight.bold), + ), + )); + } + return dropdownLists; + } +} diff --git a/demos/supabase-trello/lib/features/offlineboards/presentation/index.dart b/demos/supabase-trello/lib/features/offlineboards/presentation/index.dart new file mode 100644 index 00000000..39b8be28 --- /dev/null +++ b/demos/supabase-trello/lib/features/offlineboards/presentation/index.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/models/board.dart'; +import 'package:trelloappclone_flutter/models/workspace.dart'; +import 'package:trelloappclone_flutter/features/drawer/presentation/index.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; + +import '../../../utils/service.dart'; +import '../../../utils/widgets.dart'; + +class OfflineBoards extends StatefulWidget { + const OfflineBoards({super.key}); + + @override + State createState() => _OfflineBoardsState(); +} + +class _OfflineBoardsState extends State with Service { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Offline boards"), + actions: [IconButton(onPressed: () {}, icon: const Icon(Icons.search))], + ), + drawer: const CustomDrawer(), + body: SingleChildScrollView( + child: StreamBuilder( + stream: getWorkspacesStream(), + builder: (BuildContext context, + AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + List children = snapshot.data as List; + + if (children.isNotEmpty) { + return Column(children: buildWorkspacesAndBoards(children)); + } + } + return const SizedBox.shrink(); + })), + ); + } + + List buildWorkspacesAndBoards(List wkspcs) { + List workspacesboards = []; + Widget workspace; + + for (int i = 0; i < wkspcs.length; i++) { + workspace = ListTile( + tileColor: whiteShade, + leading: Text(wkspcs[i].name), + ); + + workspacesboards.add(workspace); + + workspacesboards.add(StreamBuilder( + stream: getBoardsStream(wkspcs[i].id), + builder: (BuildContext context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + List children = snapshot.data as List; + + if (children.isNotEmpty) { + return Column(children: buildBoards(children, wkspcs[i])); + } + } + return const SizedBox.shrink(); + })); + } + // } + return workspacesboards; + } + + List buildBoards(List brd, Workspace wkspcs) { + List boards = []; + for (int j = 0; j < brd.length; j++) { + boards.add(ListTile( + leading: ColorSquare(bckgrd: brd[j].background), + title: Text(brd[j].name), + onTap: () {}, + trailing: Switch( + value: brd[j].availableOffline ?? false, + activeColor: brandColor, + onChanged: (bool value) { + setState(() { + brd[j].availableOffline = value; + updateOfflineStatus(brd[j]); + }); + }, + ), + )); + } + return boards; + } +} diff --git a/demos/supabase-trello/lib/features/powerups/presentation/index.dart b/demos/supabase-trello/lib/features/powerups/presentation/index.dart new file mode 100644 index 00000000..a657bc34 --- /dev/null +++ b/demos/supabase-trello/lib/features/powerups/presentation/index.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +import '../../../utils/constant.dart'; + +class PowerUps extends StatefulWidget { + const PowerUps({super.key}); + + @override + State createState() => _PowerUpsState(); +} + +class _PowerUpsState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Power-Ups"), + centerTitle: false, + ), + body: ListView.separated( + itemBuilder: (BuildContext context, index) { + return ListTile( + leading: const CircleAvatar(), + title: Text(powerups[index]["title"]!), + subtitle: Text(powerups[index]["description"]!), + trailing: Switch( + value: false, + onChanged: ((value) {}), + ), + ); + }, + separatorBuilder: (context, index) => const Divider( + color: Colors.black, + ), + itemCount: powerups.length), + ); + } +} diff --git a/demos/supabase-trello/lib/features/settings/presentation/index.dart b/demos/supabase-trello/lib/features/settings/presentation/index.dart new file mode 100644 index 00000000..aa360798 --- /dev/null +++ b/demos/supabase-trello/lib/features/settings/presentation/index.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/features/drawer/presentation/index.dart'; + +class Settings extends StatefulWidget { + const Settings({super.key}); + + @override + State createState() => _SettingsState(); +} + +class _SettingsState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Settings"), + ), + drawer: const CustomDrawer(), + body: SingleChildScrollView( + child: Column( + children: [ + const ListTile( + subtitle: Text("Notifications"), + ), + const ListTile( + title: Text("Open system settings"), + ), + const Divider( + height: 2, + thickness: 2, + ), + const ListTile( + subtitle: Text("Application theme"), + ), + const ListTile( + title: Text("Select theme"), + ), + const Divider( + height: 2, + thickness: 2, + ), + const ListTile( + subtitle: Text("Accessibility"), + ), + ListTile( + title: const Text("Color blind friendly mode"), + trailing: Checkbox(value: false, onChanged: ((value) {})), + ), + ListTile( + title: const Text("Enable animations"), + trailing: Checkbox(value: true, onChanged: ((value) {})), + ), + ListTile( + title: const Text("Show label names on card front"), + trailing: Checkbox(value: false, onChanged: ((value) {})), + ), + const ListTile( + subtitle: Text("Sync"), + ), + const ListTile( + title: Text("Sync queue"), + ), + const ListTile( + subtitle: Text("General"), + ), + const ListTile( + title: Text("Profile and visibility"), + ), + const ListTile( + title: Text("Create card details"), + ), + const ListTile( + title: Text("Set app language"), + ), + ListTile( + title: const Text("Delete account"), + onTap: () { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text( + "Delete account?", + style: TextStyle(fontWeight: FontWeight.bold), + ), + content: const Text( + "You must log in on the web to delete your account"), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text("CANCEL")), + TextButton( + onPressed: () {}, child: const Text("GO TO WEB")) + ], + ); + }); + }, + ), + const ListTile( + title: Text("About Trello"), + ), + const ListTile( + title: Text("More Atlassian apps"), + ), + const ListTile( + title: Text("Contact support"), + ), + Padding( + padding: const EdgeInsets.only(bottom: 30.0), + child: ListTile( + title: const Text("Log out"), + onTap: () { + Navigator.pushNamed(context, '/'); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/demos/supabase-trello/lib/features/signtotrello/domain/sign_arguments.dart b/demos/supabase-trello/lib/features/signtotrello/domain/sign_arguments.dart new file mode 100644 index 00000000..52fa78a0 --- /dev/null +++ b/demos/supabase-trello/lib/features/signtotrello/domain/sign_arguments.dart @@ -0,0 +1,5 @@ +class SignArguments { + final Enum type; + + SignArguments(this.type); +} \ No newline at end of file diff --git a/demos/supabase-trello/lib/features/signtotrello/presentation/index.dart b/demos/supabase-trello/lib/features/signtotrello/presentation/index.dart new file mode 100644 index 00000000..6eb78bd5 --- /dev/null +++ b/demos/supabase-trello/lib/features/signtotrello/presentation/index.dart @@ -0,0 +1,191 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/features/signtotrello/domain/sign_arguments.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; + +import '../../../utils/config.dart'; +import '../../../utils/service.dart'; + +class SignToTrello extends StatefulWidget { + const SignToTrello({super.key}); + + @override + State createState() => _SignToTrelloState(); + + static const routeName = '/sign'; +} + +class _SignToTrelloState extends State with Service { + final TextEditingController emailcontroller = TextEditingController(); + final TextEditingController usernamecontroller = TextEditingController(); + final TextEditingController passwordcontroller = TextEditingController(); + final TextEditingController confirmcontroller = TextEditingController(); + + @override + Widget build(BuildContext context) { + final args = ModalRoute.of(context)!.settings.arguments as SignArguments; + + return Scaffold( + appBar: AppBar( + title: Text( + (args.type == Sign.signUp) ? "Sign up" : " Log in to continue"), + centerTitle: false, + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only(top: 10.0), + child: Column( + children: [ + Image.asset( + logo, + width: 30, + height: 30, + ), + Padding( + padding: const EdgeInsets.only(bottom: 10.0, top: 10.0), + child: Text( + (args.type == Sign.signUp) + ? "Sign up to continue" + : "Log in to continue", + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + Padding( + padding: + const EdgeInsets.only(left: 20.0, right: 20.0, top: 10.0), + child: TextField( + controller: emailcontroller, + textCapitalization: TextCapitalization.none, + keyboardType: TextInputType.emailAddress, + decoration: + const InputDecoration(hintText: "Enter your email"), + ), + ), + (args.type == Sign.signUp) + ? Padding( + padding: const EdgeInsets.only( + left: 20.0, right: 20.0, top: 10.0), + child: TextField( + controller: usernamecontroller, + keyboardType: TextInputType.name, + decoration: + const InputDecoration(hintText: "Enter your name"), + ), + ) + : const SizedBox.shrink(), + Padding( + padding: const EdgeInsets.only( + left: 20.0, right: 20.0, top: 10.0, bottom: 10.0), + child: TextField( + controller: passwordcontroller, + obscureText: true, + decoration: + const InputDecoration(hintText: "Enter your password"), + ), + ), + (args.type == Sign.signUp) + ? Padding( + padding: const EdgeInsets.only( + left: 20.0, right: 20.0, top: 10.0), + child: TextField( + controller: confirmcontroller, + obscureText: true, + decoration: const InputDecoration( + hintText: "Confirm your password"), + )) + : const SizedBox.shrink(), + (args.type == Sign.signUp) + ? const Padding( + padding: EdgeInsets.all(20.0), + child: Text( + "By signing up, I accept the Atlassian Cloud Terms of Service and acknowledge the Privacy Policy"), + ) + : const SizedBox.shrink(), + SizedBox( + width: MediaQuery.of(context).size.width * 0.8, + height: 50, + child: ElevatedButton( + onPressed: () { + if (args.type == Sign.signUp && validateSignUp()) { + signUp( + name: usernamecontroller.text, + email: emailcontroller.text, + password: encryptPassword(passwordcontroller.text), + context: context); + } else if (args.type == Sign.logIn && validateLogin()) { + logIn(emailcontroller.text, + encryptPassword(passwordcontroller.text), context); + } + }, + style: + ElevatedButton.styleFrom(backgroundColor: brandColor), + child: Text( + (args.type == Sign.signUp) ? "Sign up" : "Log in")), + ), + // ListTile( + // onTap: () {}, + // leading: Icon( + // MdiIcons.google, + // color: brandColor, + // ), + // title: const Text( + // "CONTINUE WITH GOOGLE", + // style: TextStyle(fontWeight: FontWeight.bold), + // ), + // ), + // ListTile( + // onTap: () {}, + // leading: Icon( + // MdiIcons.microsoft, + // color: brandColor, + // ), + // title: const Text( + // "CONTINUE WITH MICROSOFT", + // style: TextStyle(fontWeight: FontWeight.bold), + // ), + // ), + // ListTile( + // onTap: () {}, + // leading: Icon( + // MdiIcons.apple, + // color: brandColor, + // ), + // title: const Text( + // "CONTINUE WITH APPLE", + // style: TextStyle(fontWeight: FontWeight.bold), + // ), + // ), + // GestureDetector( + // onTap: () {}, + // child: Text( + // (args.type == Sign.signUp) + // ? "Already have an Atlassian account? Log in" + // : "Can't log in? Create an account", + // style: const TextStyle( + // decoration: TextDecoration.underline, color: brandColor), + // ), + // ) + ], + ), + ), + ), + ); + } + + bool validateSignUp() { + if (emailcontroller.text.isNotEmpty && + emailcontroller.text.isNotEmpty && + passwordcontroller.text.isNotEmpty && + confirmcontroller.text.isNotEmpty && + confirmcontroller.text == passwordcontroller.text) { + return true; + } + return false; + } + + bool validateLogin() { + if (emailcontroller.text.isNotEmpty && passwordcontroller.text.isNotEmpty) { + return true; + } + return false; + } +} diff --git a/demos/supabase-trello/lib/features/viewmembers/presentation/index.dart b/demos/supabase-trello/lib/features/viewmembers/presentation/index.dart new file mode 100644 index 00000000..d35a8b9a --- /dev/null +++ b/demos/supabase-trello/lib/features/viewmembers/presentation/index.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +class ViewMembers extends StatefulWidget { + const ViewMembers({super.key}); + + @override + State createState() => _ViewMembersState(); +} + +class _ViewMembersState extends State { + List cardMembers = [ + {"name": "Jane Doe", "handle": "@janedoe"} + ]; + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text("Card Members"), + content: SizedBox( + height: 80, + child: Column(children: buildWidgets()), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text("DONE")) + ], + ); + } + + List buildWidgets() { + List members = []; + for (int i = 0; i < cardMembers.length; i++) { + members.add(ListTile( + leading: const CircleAvatar(), + title: Text(cardMembers[i]["name"]), + subtitle: Text(cardMembers[i]["handle"]), + )); + } + return members; + } +} diff --git a/demos/supabase-trello/lib/features/visibility/presentation/index.dart b/demos/supabase-trello/lib/features/visibility/presentation/index.dart new file mode 100644 index 00000000..a77394bd --- /dev/null +++ b/demos/supabase-trello/lib/features/visibility/presentation/index.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +import '../../../utils/constant.dart'; + +class BoardVisibility extends StatefulWidget { + const BoardVisibility({super.key}); + + @override + State createState() => _BoardVisibilityState(); +} + +class _BoardVisibilityState extends State { + List checked = [false, false, false]; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text("Visibility"), + content: SizedBox( + height: 360, + child: Column( + children: [ + Card( + child: ListTile( + leading: Checkbox( + value: checked[0], + onChanged: (bool? value) {}, + ), + title: Text(visibilityConfigurations[0]["type"]!), + subtitle: Text(visibilityConfigurations[0]["description"]!), + ), + ), + Card( + child: ListTile( + leading: Checkbox( + value: checked[1], + onChanged: (bool? value) {}, + ), + title: Text(visibilityConfigurations[1]["type"]!), + subtitle: Text(visibilityConfigurations[1]["description"]!), + ), + ), + Card( + child: ListTile( + leading: Checkbox( + value: checked[2], + onChanged: (bool? value) {}, + ), + title: Text(visibilityConfigurations[2]["type"]!), + subtitle: Text(visibilityConfigurations[2]["description"]!), + ), + ) + ], + ), + ), + actions: [ElevatedButton(onPressed: () {}, child: const Text("Save"))], + ); + } +} diff --git a/demos/supabase-trello/lib/features/workspace/domain/workspace_arguments.dart b/demos/supabase-trello/lib/features/workspace/domain/workspace_arguments.dart new file mode 100644 index 00000000..cdea7d12 --- /dev/null +++ b/demos/supabase-trello/lib/features/workspace/domain/workspace_arguments.dart @@ -0,0 +1,7 @@ +import 'package:trelloappclone_flutter/models/workspace.dart'; + +class WorkspaceArguments { + final Workspace wkspc; + + WorkspaceArguments(this.wkspc); +} diff --git a/demos/supabase-trello/lib/features/workspace/presentation/index.dart b/demos/supabase-trello/lib/features/workspace/presentation/index.dart new file mode 100644 index 00000000..4f23e377 --- /dev/null +++ b/demos/supabase-trello/lib/features/workspace/presentation/index.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/models/board.dart'; +import 'package:trelloappclone_flutter/features/drawer/presentation/index.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; + +import '../../../utils/service.dart'; +import '../domain/workspace_arguments.dart'; + +class WorkspaceScreen extends StatefulWidget { + const WorkspaceScreen({super.key}); + + @override + State createState() => _WorkspaceScreenState(); + + static const routeName = '/workspace'; +} + +class _WorkspaceScreenState extends State with Service { + @override + Widget build(BuildContext context) { + final args = + ModalRoute.of(context)!.settings.arguments as WorkspaceArguments; + + return Scaffold( + appBar: AppBar( + title: Text(args.wkspc.name), + actions: [ + IconButton(onPressed: () {}, icon: const Icon(Icons.search)), + IconButton( + onPressed: () {}, + icon: const Icon(Icons.notifications_none_outlined)) + ], + ), + drawer: const CustomDrawer(), + body: DefaultTabController( + length: 2, + initialIndex: 0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const TabBar( + labelColor: brandColor, + unselectedLabelColor: themeColor, + tabs: [ + Tab( + text: "BOARDS", + ), + Tab( + text: "HIGHLIGHTS", + ) + ]), + SizedBox( + height: MediaQuery.of(context).size.height * 0.8, + child: TabBarView(children: [ + Column( + children: [ + Container( + width: MediaQuery.of(context).size.width, + height: 50, + color: whiteShade, + alignment: Alignment.centerLeft, + child: const Padding( + padding: EdgeInsets.only(left: 8.0), + child: Text("Your Workspace boards")), + ), + StreamBuilder( + stream: getBoardsStream(args.wkspc.id), + builder: (context, snapshot) { + if (snapshot.hasData) { + List children = snapshot.data as List; + + if (children.isNotEmpty) { + return Expanded( + child: GridView.builder( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 1 / 0.7), + itemCount: children.length, + itemBuilder: + (BuildContext context, int index) { + return GestureDetector( + onTap: () {}, + child: Card( + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular( + 10.0)), + color: Color(int.parse( + children[index] + .background + .substring(1, 7), + radix: 16) + + 0xFF000000), + child: Align( + alignment: Alignment.bottomLeft, + child: ListTile( + tileColor: themeColor, + title: Text( + children[index].name, + style: const TextStyle( + color: Colors.white), + ), + ), + ), + ), + ); + })); + } + } + return const SizedBox.shrink(); + }, + ) + ], + ), + Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + children: [ + const ListTile( + leading: Icon( + Icons.start, + color: brandColor, + ), + title: Text("GET STARTED"), + ), + Card( + child: Column( + children: [ + Container( + color: brandColor, + height: 100, + ), + const Padding( + padding: EdgeInsets.all(8.0), + child: Text( + "Stay on track and up-to-date", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18), + ), + ), + const Padding( + padding: EdgeInsets.all(8.0), + child: Text( + "Invite people to boards and cards, add comments, and adjust due dates all from the new Trello Home. We'll show the most important activity here.", + textAlign: TextAlign.center, + ), + ) + ], + ), + ) + ], + )) + ]), + ) + ], + )), + ); + } +} diff --git a/demos/supabase-trello/lib/features/workspacemenu/presentation/index.dart b/demos/supabase-trello/lib/features/workspacemenu/presentation/index.dart new file mode 100644 index 00000000..8cff3ceb --- /dev/null +++ b/demos/supabase-trello/lib/features/workspacemenu/presentation/index.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/service.dart'; + +import '../../../main.dart'; +import '../../../utils/color.dart'; + +class WorkspaceMenu extends StatefulWidget { + const WorkspaceMenu({super.key}); + + @override + State createState() => _WorkspaceMenuState(); +} + +class _WorkspaceMenuState extends State with Service { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: const Icon( + Icons.close, + size: 30, + ), + ), + actions: [ + IconButton( + onPressed: () { + Navigator.pushNamed(context, '/workspacesettings'); + }, + icon: const Icon(Icons.settings)) + ], + title: const Text("Workspace menu"), + centerTitle: false, + ), + body: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + trello.selectedWorkspace.name, + style: const TextStyle( + fontWeight: FontWeight.bold, fontSize: 18), + ), + const Text.rich(TextSpan(children: [ + WidgetSpan( + child: + Icon(Icons.lock, color: dangerColor, size: 15)), + TextSpan( + text: "Public", + style: TextStyle(color: dangerColor)) + ])), + Padding( + padding: const EdgeInsets.only(top: 8.0, bottom: 8.0), + child: Text(trello.selectedWorkspace.description), + ), + ], + ), + const Spacer(), + CircleAvatar( + radius: 30, + backgroundColor: Colors.green[400], + child: Text( + trello.selectedWorkspace.name[0].toUpperCase(), + style: const TextStyle(color: whiteShade), + ), + ) + ], + ), + Container( + padding: const EdgeInsets.only(top: 10.0), + child: ListTile( + tileColor: whiteShade, + leading: const Icon(Icons.person_outline), + title: const Padding( + padding: EdgeInsets.only(top: 10.0, bottom: 15), + child: Text("Members"), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 18.0), + child: InkWell( + onTap: () { + Navigator.pushNamed(context, '/members'); + }, + child: Row( + children: buildMemberAvatars(), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 15.0), + child: SizedBox( + height: 37, + width: MediaQuery.of(context).size.width * 0.7, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: brandColor), + onPressed: () { + Navigator.pushNamed(context, '/invitemember'); + }, + child: const Text("Invite")), + ), + ) + ], + ), + ), + ) + ], + ), + )); + } + + List buildMemberAvatars() { + List avatars = []; + + trello.selectedWorkspace.members?.forEach((member) { + avatars.add( + CircleAvatar( + backgroundColor: brandColor, + child: Text(member.name[0].toUpperCase()), + ) + ); + avatars.add( + const SizedBox(width: 4,) + ); + }); + return avatars; + } +} diff --git a/demos/supabase-trello/lib/features/workspacesettings/presentation/index.dart b/demos/supabase-trello/lib/features/workspacesettings/presentation/index.dart new file mode 100644 index 00000000..0a286fa1 --- /dev/null +++ b/demos/supabase-trello/lib/features/workspacesettings/presentation/index.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; + +import '../../../main.dart'; +import '../../visibility/presentation/index.dart'; + +class WorkspaceSettings extends StatefulWidget { + const WorkspaceSettings({super.key}); + + @override + State createState() => _WorkspaceSettingsState(); +} + +class _WorkspaceSettingsState extends State { + final TextEditingController nameController = TextEditingController(); + + @override + Widget build(BuildContext context) { + nameController.text = trello.selectedWorkspace.name; + return Scaffold( + appBar: AppBar( + title: const Text("Workspace settings"), + centerTitle: false, + ), + body: Column( + children: [ + ListTile( + leading: const Text("Name"), + trailing: SizedBox( + width: MediaQuery.of(context).size.width * 0.4, + child: EditableText( + textAlign: TextAlign.end, + controller: nameController, + focusNode: FocusNode(), + style: const TextStyle( + fontWeight: FontWeight.bold, color: brandColor), + cursorColor: brandColor, + backgroundCursorColor: brandColor, + onSubmitted: (value) { + Navigator.pushNamed(context, '/home'); + }, + )), + ), + ListTile( + leading: const Text("Visibility"), + trailing: GestureDetector( + child: const Text("Public"), + onTap: () { + showDialog( + context: context, + builder: (BuildContext context) { + return const BoardVisibility(); + }); + }, + ), + ), + const Align( + alignment: Alignment.center, + child: Text("Not all settings are editable on mobile"), + ) + ], + ), + ); + } +} diff --git a/demos/supabase-trello/lib/main.dart b/demos/supabase-trello/lib/main.dart new file mode 100644 index 00000000..c5534a4c --- /dev/null +++ b/demos/supabase-trello/lib/main.dart @@ -0,0 +1,117 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:provider/provider.dart'; +import 'package:trelloappclone_flutter/features/generateworkspace/presentation/index.dart'; +import 'package:trelloappclone_flutter/utils/trello_provider.dart'; +import 'package:trelloappclone_flutter/models/user.dart'; +import 'package:trelloappclone_flutter/protocol/data_client.dart'; +import 'package:logging/logging.dart'; + +import 'features/aboutboard/presentation/index.dart'; +import 'features/archivedcards/presentation/index.dart'; +import 'features/archivedlists/presentation/index.dart'; +import 'features/board/presentation/index.dart'; +import 'features/boardbackground/presentation/index.dart'; +import 'features/boardmenu/presentation/index.dart'; +import 'features/boardsettings/presentation/index.dart'; +import 'features/carddetails/presentation/index.dart'; +import 'features/copyboard/presentation/index.dart'; +import 'features/createboard/presentation/index.dart'; +import 'features/createcard/presentation/index.dart'; +import 'features/createworkspace/presentation/index.dart'; +import 'features/emailtoboard/presentation/index.dart'; +import 'features/home/presentation/index.dart'; +import 'features/invitemember/presentation/index.dart'; +import 'features/landing/presentation/index.dart'; +import 'features/members/presentation/index.dart'; +import 'features/mycards/presentation/index.dart'; +import 'features/notifications/presentation/index.dart'; +import 'features/offlineboards/presentation/index.dart'; +import 'features/powerups/presentation/index.dart'; +import 'features/settings/presentation/index.dart'; +import 'features/signtotrello/presentation/index.dart'; +import 'features/workspace/presentation/index.dart'; +import 'features/workspacemenu/presentation/index.dart'; +import 'features/workspacesettings/presentation/index.dart'; + +// Sets up a singleton client object that can be used to talk to the server from +// anywhere in our app. +// The client is set up to connect to a Powersync project already set up. +var dataClient = DataClient(); + +TrelloProvider trello = TrelloProvider(); + +void main() async { + // Log info from PowerSync + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { + // if (kDebugMode) { + print( + '[${record.loggerName}] ${record.level.name}: ${record.time}: ${record.message}'); + + if (record.error != null) { + print(record.error); + } + if (record.stackTrace != null) { + print(record.stackTrace); + } + // } + }); + + WidgetsFlutterBinding + .ensureInitialized(); //required to get sqlite filepath from path_provider before UI has initialized + await dataClient.initialize(); + if (dataClient.isLoggedIn()) { + TrelloUser? user = await dataClient.getLoggedInUser(); + trello.setUser(user!); + } + runApp(ChangeNotifierProvider( + create: (context) => TrelloProvider(), child: const MyApp())); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Trello App Clone', + theme: ThemeData( + textTheme: Theme.of(context).textTheme.apply( + fontFamily: GoogleFonts.poppins().fontFamily, + ), + ), + initialRoute: dataClient.isLoggedIn() ? '/home' : '/', + routes: { + '/': (context) => const Landing(), + '/home': (context) => const Home(), + '/notifications': (context) => const Notifications(), + '/workspacemenu': (context) => const WorkspaceMenu(), + '/workspacesettings': (context) => const WorkspaceSettings(), + '/members': (context) => const Members(), + '/invitemember': (context) => const InviteMember(), + '/createworkspace': (context) => const CreateWorkspace(), + '/createboard': (context) => const CreateBoard(), + '/generateworkspace': (context) => const GenerateWorkspace(), + '/boardbackground': (context) => const BoardBackground(), + '/createcard': (context) => const CreateCard(), + BoardScreen.routeName: (context) => const BoardScreen(), + '/boardmenu': (context) => const BoardMenu(), + '/copyboard': (context) => const CopyBoard(), + '/boardsettings': (context) => const BoardSettings(), + '/archivedlists': (context) => const ArchivedLists(), + '/archivedcards': (context) => const ArchivedCards(), + '/emailtoboard': (context) => const EmailToBoard(), + '/aboutboard': (context) => const AboutBoard(), + '/powerups': (context) => const PowerUps(), + CardDetails.routeName: (context) => const CardDetails(), + '/mycards': (context) => const MyCards(), + '/offlineboards': (context) => const OfflineBoards(), + '/settings': (context) => const Settings(), + SignToTrello.routeName: (context) => const SignToTrello(), + '/workspace': (context) => const WorkspaceScreen() + }); + } +} diff --git a/demos/supabase-trello/lib/models/activity.dart b/demos/supabase-trello/lib/models/activity.dart new file mode 100644 index 00000000..28a7a380 --- /dev/null +++ b/demos/supabase-trello/lib/models/activity.dart @@ -0,0 +1,39 @@ +import 'package:powersync/sqlite3.dart' as sqlite; + +class Activity { + final String id; + final String workspaceId; + + final String? boardId; + + final String userId; + + final String? cardId; + + final String description; + + final DateTime dateCreated; + + Activity({ + required this.id, + required this.workspaceId, + this.boardId, + required this.userId, + this.cardId, + required this.description, + required this.dateCreated, + }); + + factory Activity.fromRow(sqlite.Row row) { + return Activity( + id: row['id'], + workspaceId: row['workspaceId'], + boardId: row['boardId'], + userId: row['userId'], + cardId: row['cardId'], + description: row['description'], + dateCreated: DateTime.parse(row['dateCreated']) + ); + } + +} diff --git a/demos/supabase-trello/lib/models/attachment.dart b/demos/supabase-trello/lib/models/attachment.dart new file mode 100644 index 00000000..a2cb5b2c --- /dev/null +++ b/demos/supabase-trello/lib/models/attachment.dart @@ -0,0 +1,28 @@ +import 'package:powersync/sqlite3.dart' as sqlite; + +class Attachment { + Attachment({ + required this.id, + required this.workspaceId, + required this.userId, + required this.cardId, + required this.attachment, + }); + + final String id; + final String workspaceId; + + final String userId; + + final String cardId; + + final String attachment; + + factory Attachment.fromRow(sqlite.Row row) { + return Attachment( + id: row['id'], + workspaceId: row['workspaceId'], + userId: row['userId'], + cardId: row['cardId'], + attachment: row['attachment']);} +} diff --git a/demos/supabase-trello/lib/models/board.dart b/demos/supabase-trello/lib/models/board.dart new file mode 100644 index 00000000..a9bbe8f8 --- /dev/null +++ b/demos/supabase-trello/lib/models/board.dart @@ -0,0 +1,86 @@ +import 'package:powersync/sqlite3.dart' as sqlite; +import 'package:trelloappclone_flutter/models/board_label.dart'; + +class Board { + Board( + {required this.id, + required this.workspaceId, + required this.userId, + required this.name, + this.description, + required this.visibility, + required this.background, + this.starred, + this.enableCover, + this.watch, + this.availableOffline, + this.label, + this.emailAddress, + this.commenting, + this.memberType, + this.pinned, + this.selfJoin, + this.close, + this.boardLabels}); + + final String id; + + final String workspaceId; + + final String userId; + + final String name; + + final String? description; + + final String visibility; + + final String background; + + final bool? starred; + + final bool? enableCover; + + final bool? watch; + + bool? availableOffline; + + final String? label; + + final String? emailAddress; + + final int? commenting; + + final int? memberType; + + final bool? pinned; + + final bool? selfJoin; + + final bool? close; + + List? boardLabels; + + factory Board.fromRow(sqlite.Row row) { + return Board( + id: row['id'], + workspaceId: row['workspaceId'], + userId: row['userId'], + name: row['name'], + description: row['description'], + visibility: row['visibility'], + background: row['background'], + starred: row['starred'] == 1, + enableCover: row['enableCover'] == 1, + watch: row['watch'] == 1, + availableOffline: row['availableOffline'] == 1, + label: row['label'], + emailAddress: row['emailAddress'], + commenting: row['commenting'], + memberType: row['memberType'], + pinned: row['pinned'] == 1, + selfJoin: row['selfJoin'] == 1, + close: row['close'] == 1, + boardLabels: []); + } +} diff --git a/demos/supabase-trello/lib/models/board_label.dart b/demos/supabase-trello/lib/models/board_label.dart new file mode 100644 index 00000000..5b63fcee --- /dev/null +++ b/demos/supabase-trello/lib/models/board_label.dart @@ -0,0 +1,34 @@ +import 'package:powersync/sqlite3.dart' as sqlite; + +class BoardLabel { + final String id; + + final String boardId; + + final String workspaceId; + + late String title; + + final String color; + + final DateTime dateCreated; + + BoardLabel({ + required this.id, + required this.boardId, + required this.workspaceId, + required this.title, + required this.color, + required this.dateCreated, + }); + + factory BoardLabel.fromRow(sqlite.Row row) { + return BoardLabel( + id: row['id'], + boardId: row['boardId'], + workspaceId: row['workspaceId'], + title: row['title'], + color: row['color'], + dateCreated: DateTime.parse(row['dateCreated'])); + } +} diff --git a/demos/supabase-trello/lib/models/card.dart b/demos/supabase-trello/lib/models/card.dart new file mode 100644 index 00000000..39ff31f0 --- /dev/null +++ b/demos/supabase-trello/lib/models/card.dart @@ -0,0 +1,68 @@ +import 'package:powersync/sqlite3.dart' as sqlite; +import 'package:trelloappclone_flutter/models/card_label.dart'; + +class Cardlist { + Cardlist({ + required this.id, + required this.workspaceId, + required this.listId, + required this.userId, + required this.name, + this.description, + this.startDate, + this.dueDate, + required this.rank, + this.attachment, + this.archived, + this.checklist, + this.comments, + this.cardLabels, + }); + + final String id; + + final String workspaceId; + + String listId; + + final String userId; + + String name; + + String? description; + + final DateTime? startDate; + + final DateTime? dueDate; + + int rank; + + final bool? attachment; + + final bool? archived; + + final bool? checklist; + + final bool? comments; + + List? cardLabels; + + factory Cardlist.fromRow(sqlite.Row row) { + return Cardlist( + id: row['id'], + workspaceId: row['workspaceId'], + listId: row['listId'], + userId: row['userId'], + name: row['name'], + description: row['description'], + startDate: + row['startDate'] != null ? DateTime.parse(row['startDate']) : null, + dueDate: row['dueDate'] != null ? DateTime.parse(row['dueDate']) : null, + rank: row['rank'], + attachment: row['attachment'] == 1, + archived: row['archived'] == 1, + checklist: row['checklist'] == 1, + comments: row['comments'] == 1, + cardLabels: []); + } +} diff --git a/demos/supabase-trello/lib/models/card_label.dart b/demos/supabase-trello/lib/models/card_label.dart new file mode 100644 index 00000000..6016e150 --- /dev/null +++ b/demos/supabase-trello/lib/models/card_label.dart @@ -0,0 +1,34 @@ +import 'package:powersync/sqlite3.dart' as sqlite; + +class CardLabel { + final String id; + + final String cardId; + + final String boardId; + + final String workspaceId; + + final String boardLabelId; + + final DateTime dateCreated; + + CardLabel({ + required this.id, + required this.cardId, + required this.boardId, + required this.workspaceId, + required this.boardLabelId, + required this.dateCreated, + }); + + factory CardLabel.fromRow(sqlite.Row row) { + return CardLabel( + id: row['id'], + cardId: row['cardId'], + boardId: row['boardId'], + workspaceId: row['workspaceId'], + boardLabelId: row['boardLabelId'], + dateCreated: DateTime.parse(row['dateCreated'])); + } +} diff --git a/demos/supabase-trello/lib/models/checklist.dart b/demos/supabase-trello/lib/models/checklist.dart new file mode 100644 index 00000000..afbc969b --- /dev/null +++ b/demos/supabase-trello/lib/models/checklist.dart @@ -0,0 +1,30 @@ +import 'package:powersync/sqlite3.dart' as sqlite; + +class Checklist { + Checklist({ + required this.id, + required this.workspaceId, + required this.cardId, + required this.name, + required this.status, + }); + + final String id; + + final String workspaceId; + + final String cardId; + + final String name; + + bool status; + + factory Checklist.fromRow(sqlite.Row row) { + return Checklist( + id: row['id'], + workspaceId: row['workspaceId'], + cardId: row['cardId'], + name: row['name'], + status: row['status'] == 1); + } +} diff --git a/demos/supabase-trello/lib/models/comment.dart b/demos/supabase-trello/lib/models/comment.dart new file mode 100644 index 00000000..878f47ae --- /dev/null +++ b/demos/supabase-trello/lib/models/comment.dart @@ -0,0 +1,30 @@ +import 'package:powersync/sqlite3.dart' as sqlite; + +class Comment { + Comment({ + required this.id, + required this.workspaceId, + required this.cardId, + required this.userId, + required this.description, + }); + + final String id; + + final String workspaceId; + + final String cardId; + + final String userId; + + final String description; + + factory Comment.fromRow(sqlite.Row row) { + return Comment( + id: row['id'], + workspaceId: row['workspaceId'], + cardId: row['cardId'], + userId: row['userId'], + description: row['description']); + } +} diff --git a/demos/supabase-trello/lib/models/listboard.dart b/demos/supabase-trello/lib/models/listboard.dart new file mode 100644 index 00000000..cc5c9207 --- /dev/null +++ b/demos/supabase-trello/lib/models/listboard.dart @@ -0,0 +1,44 @@ +import 'package:powersync/sqlite3.dart' as sqlite; +import 'card.dart'; + +class Listboard { + Listboard({ + required this.id, + required this.workspaceId, + required this.boardId, + required this.userId, + required this.name, + this.archived, + this.cards, + required this.order, + }); + + final String id; + + final String workspaceId; + + final String boardId; + + final String userId; + + final String name; + + final bool? archived; + + final int order; + + List? cards; + + factory Listboard.fromRow(sqlite.Row row) { + return Listboard( + id: row['id'], + workspaceId: row['workspaceId'], + boardId: row['boardId'], + userId: row['userId'], + name: row['name'], + archived: row['archived'] == 1, + order: row['listOrder'], + cards: [] + ); + } +} diff --git a/demos/supabase-trello/lib/models/member.dart b/demos/supabase-trello/lib/models/member.dart new file mode 100644 index 00000000..ef90436b --- /dev/null +++ b/demos/supabase-trello/lib/models/member.dart @@ -0,0 +1,30 @@ +import 'package:powersync/sqlite3.dart' as sqlite; + +class Member { + Member({ + required this.id, + required this.workspaceId, + required this.userId, + required this.name, + required this.role, + }); + + final String id; + + final String workspaceId; + + final String userId; + + final String name; + + final String role; + + factory Member.fromRow(sqlite.Row row) { + return Member( + id: row['id'], + workspaceId: row['workspaceId'], + userId: row['userId'], + name: row['name'], + role: row['role']); + } +} diff --git a/demos/supabase-trello/lib/models/models.dart b/demos/supabase-trello/lib/models/models.dart new file mode 100644 index 00000000..c3c8e939 --- /dev/null +++ b/demos/supabase-trello/lib/models/models.dart @@ -0,0 +1,12 @@ +export 'activity.dart'; +export 'attachment.dart'; +export 'board.dart'; +export 'card.dart'; +export 'checklist.dart'; +export 'comment.dart'; +export 'listboard.dart'; +export 'member.dart'; +export 'user.dart'; +export 'workspace.dart'; +export 'board_label.dart'; +export 'card_label.dart'; \ No newline at end of file diff --git a/demos/supabase-trello/lib/models/schema.dart b/demos/supabase-trello/lib/models/schema.dart new file mode 100644 index 00000000..b093680e --- /dev/null +++ b/demos/supabase-trello/lib/models/schema.dart @@ -0,0 +1,149 @@ +import 'package:powersync/powersync.dart'; + +const schema = Schema(([ + //class: Activity + Table('activity', [ + Column.text('workspaceId'), + Column.text('boardId'), + Column.text('userId'), + Column.text('cardId'), + Column.text('description'), + Column.text('dateCreated'), + ], indexes: [ + Index('board', [IndexedColumn('boardId')]), + Index('user', [IndexedColumn('userId')]), + Index('card', [IndexedColumn('cardId')]) + ]), + //class: Attachment + Table('attachment', [ + Column.text('workspaceId'), + Column.text('userId'), + Column.text('cardId'), + Column.text('attachment'), + ], indexes: [ + Index('user', [IndexedColumn('userId')]), + ]), + //class: Board + Table('board', [ + Column.text('workspaceId'), + Column.text('userId'), + Column.text('name'), + Column.text('description'), + Column.text('visibility'), + Column.text('background'), + Column.integer('starred'), + Column.integer('enableCover'), + Column.integer('watch'), + Column.integer('availableOffline'), + Column.text('label'), + Column.text('emailAddress'), + Column.integer('commenting'), + Column.integer('memberType'), + Column.integer('pinned'), + Column.integer('selfJoin'), + Column.integer('close'), + ], indexes: [ + Index('workspace', [IndexedColumn('workspaceId')]), + Index('user', [IndexedColumn('userId')]), + ]), + //class: Cardlist + Table('card', [ + Column.text('workspaceId'), + Column.text('listId'), + Column.text('userId'), + Column.text('name'), + Column.text('description'), + Column.text('startDate'), + Column.text('dueDate'), + Column.integer('rank'), + Column.integer('attachment'), + Column.integer('archived'), + Column.integer('checklist'), + Column.integer('comments'), + ], indexes: [ + Index('list', [IndexedColumn('listId')]), + Index('user', [IndexedColumn('userId')]), + ]), + //class: Checklist + Table('checklist', [ + Column.text('workspaceId'), + Column.text('cardId'), + Column.text('name'), + Column.integer('status'), + ], indexes: [ + Index('card', [IndexedColumn('cardId')]), + ]), + //class: Comment + Table('comment', [ + Column.text('workspaceId'), + Column.text('cardId'), + Column.text('userId'), + Column.text('description'), + ], indexes: [ + Index('card', [IndexedColumn('cardId')]), + Index('user', [IndexedColumn('userId')]), + ]), + //class: Listboard + Table('listboard', [ + Column.text('workspaceId'), + Column.text('boardId'), + Column.text('userId'), + Column.text('name'), + Column.integer('archived'), + Column.integer('listOrder'), + ], indexes: [ + Index('board', [IndexedColumn('boardId')]), + Index('user', [IndexedColumn('userId')]), + ]), + //class: Member + Table('member', [ + Column.text('workspaceId'), + Column.text('userId'), + Column.text('name'), + Column.text('role'), + ], indexes: [ + Index('user', [IndexedColumn('userId')]), + ]), + //class: User + // table: trellouser + // fields: + // name: String? + // email: String + // password: String + Table('trellouser', [ + Column.text('name'), + Column.text('email'), + Column.text('password'), + ], indexes: [ + Index('email', [IndexedColumn('email')]), + ]), + //class: Workspace + Table('workspace', [ + Column.text('userId'), + Column.text('name'), + Column.text('description'), + Column.text('visibility'), + ], indexes: [ + Index('user', [IndexedColumn('userId')]), + ]), + // class: BoardLabel + Table('board_label', [ + Column.text('boardId'), + Column.text('workspaceId'), + Column.text('title'), + Column.text('color'), + Column.text('dateCreated'), + ], indexes: [ + Index('board', [IndexedColumn('boardId')]), + ]), + // class: CardLabel + Table('card_label', [ + Column.text('cardId'), + Column.text('boardLabelId'), + Column.text('boardId'), + Column.text('workspaceId'), + Column.text('dateCreated'), + ], indexes: [ + Index('card', [IndexedColumn('cardId')]), + ]) +])); diff --git a/demos/supabase-trello/lib/models/user.dart b/demos/supabase-trello/lib/models/user.dart new file mode 100644 index 00000000..0eec95dc --- /dev/null +++ b/demos/supabase-trello/lib/models/user.dart @@ -0,0 +1,26 @@ +import 'package:powersync/sqlite3.dart' as sqlite; + +class TrelloUser { + TrelloUser({ + required this.id, + this.name, + required this.email, + required this.password, + }); + + final String id; + + final String? name; + + final String email; + + final String password; + + factory TrelloUser.fromRow(sqlite.Row row) { + return TrelloUser( + id: row['id'], + name: row['name'], + email: row['email'], + password: row['password']); + } +} diff --git a/demos/supabase-trello/lib/models/workspace.dart b/demos/supabase-trello/lib/models/workspace.dart new file mode 100644 index 00000000..67a72574 --- /dev/null +++ b/demos/supabase-trello/lib/models/workspace.dart @@ -0,0 +1,36 @@ +import 'package:powersync/sqlite3.dart' as sqlite; + +import 'member.dart'; + +class Workspace { + Workspace({ + required this.id, + required this.userId, + required this.name, + required this.description, + required this.visibility, + this.members, + }); + + final String id; + + final String userId; + + final String name; + + final String description; + + final String visibility; + + List? members; + + factory Workspace.fromRow(sqlite.Row row) { + return Workspace( + id: row['id'], + userId: row['userId'], + name: row['name'], + description: row['description'], + visibility: row['visibility'], + members: [] + );} +} diff --git a/demos/supabase-trello/lib/protocol/data_client.dart b/demos/supabase-trello/lib/protocol/data_client.dart new file mode 100644 index 00000000..47acf183 --- /dev/null +++ b/demos/supabase-trello/lib/protocol/data_client.dart @@ -0,0 +1,854 @@ +library powersync_client; + +import 'package:powersync/powersync.dart'; + +import "../models/models.dart"; +import 'powersync.dart'; + +export "../models/models.dart"; + +class _Repository { + DataClient client; + + _Repository(this.client); + + int boolAsInt(bool? value) { + if (value == null) { + return 0; + } + return value ? 1 : 0; + } +} + +class _ActivityRepository extends _Repository { + _ActivityRepository(DataClient client) : super(client); + + Future createActivity(Activity activity) async { + final results = await client.getDBExecutor().execute('''INSERT INTO + activity(id, workspaceId, boardId, userId, cardId, description, dateCreated) + VALUES(?, ?, ?, ?, ?, ?, datetime()) + RETURNING *''', [ + activity.id, + activity.workspaceId, + activity.boardId, + activity.userId, + activity.cardId, + activity.description + ]); + return results.isNotEmpty; + } + + Future> getActivities(Cardlist cardlist) async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM activity WHERE cardId = ? ORDER BY dateCreated DESC + ''', [cardlist.id]); + return results.map((row) => Activity.fromRow(row)).toList(); + } +} + +class _AttachmentRepository extends _Repository { + _AttachmentRepository(DataClient client) : super(client); + + Future addAttachment(Attachment attachment) async { + final results = await client.getDBExecutor().execute('''INSERT INTO + attachment(id, workspaceId, userId, cardId, attachment) + VALUES(?, ?, ?, ?, ?) + RETURNING *''', [ + attachment.id, + attachment.workspaceId, + attachment.userId, + attachment.cardId, + attachment.attachment + ]); + if (results.isEmpty) { + throw Exception("Failed to add attachment"); + } else { + return Attachment.fromRow(results.first); + } + } + + //TODO: need to replace with file upload service calls to Supabase + Future getUploadDescription(String path) => + Future.value('TODO: implement getUploadDescription'); + + Future verifyUpload(String path) => Future.value(false); +} + +class _BoardRepository extends _Repository { + _BoardRepository(DataClient client) : super(client); + + String get insertQuery => ''' + INSERT INTO + board(id, userId, workspaceId, name, description, visibility, background, starred, enableCover, watch, availableOffline, label, emailAddress, commenting, memberType, pinned, selfJoin, close) + VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18) + '''; + + String get updateQuery => ''' + UPDATE board + set userId = ?1, workspaceId = ?2, name = ?3, description = ?4, visibility = ?5, background = ?6, starred = ?7, enableCover = ?8, watch = ?9, availableOffline = ?10, label = ?11, emailAddress = ?12, commenting = ?13, memberType = ?14, pinned = ?15, selfJoin = ?16, close = ?17 + WHERE id = ?18 + '''; + + Future createBoard(Board board) async { + final results = await client.getDBExecutor().execute(''' + $insertQuery RETURNING *''', [ + board.id, + board.userId, + board.workspaceId, + board.name, + board.description, + board.visibility, + board.background, + boolAsInt(board.starred), + boolAsInt(board.enableCover), + boolAsInt(board.watch), + boolAsInt(board.availableOffline), + board.label, + board.emailAddress, + board.commenting, + board.memberType, + boolAsInt(board.pinned), + boolAsInt(board.selfJoin), + boolAsInt(board.close) + ]); + if (results.isEmpty) { + throw Exception("Failed to add Board"); + } else { + return Board.fromRow(results.first); + } + } + + Future updateBoard(Board board) async { + await client.getDBExecutor().execute(updateQuery, [ + board.userId, + board.workspaceId, + board.name, + board.description, + board.visibility, + board.background, + boolAsInt(board.starred), + boolAsInt(board.enableCover), + boolAsInt(board.watch), + boolAsInt(board.availableOffline), + board.label, + board.emailAddress, + board.commenting, + board.memberType, + boolAsInt(board.pinned), + boolAsInt(board.selfJoin), + boolAsInt(board.close), + board.id + ]); + return true; + } + + Future deleteBoard(Board board) async { + await client + .getDBExecutor() + .execute('DELETE FROM board WHERE id = ?', [board.id]); + return true; + } + + Future getWorkspaceForBoard(Board board) async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM workspace WHERE id = ? + ''', [board.workspaceId]); + return results.map((row) => Workspace.fromRow(row)).firstOrNull; + } + + Future> getAllBoards() async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM board + '''); + return results.map((row) => Board.fromRow(row)).toList(); + } +} + +class _CardlistRepository extends _Repository { + _CardlistRepository(DataClient client) : super(client); + + String get insertQuery => ''' + INSERT INTO + card(id, workspaceId, listId, userId, name, description, startDate, dueDate, attachment, archived, checklist, comments, rank) + VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13) + '''; + + Future createCard(Cardlist cardlist) async { + final results = + await client.getDBExecutor().execute('$insertQuery RETURNING *', [ + cardlist.id, + cardlist.workspaceId, + cardlist.listId, + cardlist.userId, + cardlist.name, + cardlist.description, + cardlist.startDate, + cardlist.dueDate, + boolAsInt(cardlist.attachment), + boolAsInt(cardlist.archived), + boolAsInt(cardlist.checklist), + boolAsInt(cardlist.comments), + cardlist.rank, + ]); + if (results.isEmpty) { + throw Exception("Failed to add Cardlist"); + } else { + return Cardlist.fromRow(results.first); + } + } + + String get updateQuery => ''' + UPDATE card + set listId = ?1, userId = ?2, name = ?3, description = ?4, startDate = ?5, dueDate = ?6, attachment = ?7, archived = ?8, checklist = ?9, comments = ?10, rank = ?11 + WHERE id = ?12 + '''; + + Future updateCard(Cardlist cardlist) async { + await client.getDBExecutor().execute(updateQuery, [ + cardlist.listId, + cardlist.userId, + cardlist.name, + cardlist.description, + cardlist.startDate, + cardlist.dueDate, + boolAsInt(cardlist.attachment), + boolAsInt(cardlist.archived), + boolAsInt(cardlist.checklist), + boolAsInt(cardlist.comments), + cardlist.rank, + cardlist.id + ]); + return true; + } + + Future deleteCard(Cardlist cardlist) async { + await client + .getDBExecutor() + .execute('DELETE FROM card WHERE id = ?', [cardlist.id]); + return true; + } + + Future> getCardsforList(String listId) async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM card WHERE listId = ? AND archived = 0 ORDER BY rank ASC + ''', [listId]); + return results.map((row) => Cardlist.fromRow(row)).toList(); + } +} + +class _CheckListRepository extends _Repository { + _CheckListRepository(DataClient client) : super(client); + + String get insertQuery => ''' + INSERT INTO + checklist(id, workspaceId, cardId, name, status) + VALUES(?1, ?2, ?3, ?4, ?5) + '''; + + Future createChecklist(Checklist checklist) async { + final results = + await client.getDBExecutor().execute('$insertQuery RETURNING *', [ + checklist.id, + checklist.workspaceId, + checklist.cardId, + checklist.name, + boolAsInt(checklist.status) + ]); + if (results.isEmpty) { + throw Exception("Failed to add Checklist"); + } else { + return Checklist.fromRow(results.first); + } + } + + String get updateQuery => ''' + UPDATE checklist + set cardId = ?1, name = ?2, status = ?3 + WHERE id = ?4 + '''; + + Future updateChecklist(Checklist checklist) async { + await client.getDBExecutor().execute(updateQuery, [ + checklist.cardId, + checklist.name, + boolAsInt(checklist.status), + checklist.id + ]); + return true; + } + + Future deleteChecklistItem(Checklist checklist) async { + await client + .getDBExecutor() + .execute('DELETE FROM checklist WHERE id = ?', [checklist.id]); + return true; + } + + Future> getChecklists(Cardlist crd) async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM checklist WHERE cardId = ? + ''', [crd.id]); + return results.map((row) => Checklist.fromRow(row)).toList(); + } + + Future deleteChecklist(Cardlist crd) async { + final results = await client.getDBExecutor().execute(''' + SELECT COUNT(*) FROM checklist WHERE cardId = ? + ''', [crd.id]); + await client + .getDBExecutor() + .execute('DELETE FROM checklist WHERE cardId = ?', [crd.id]); + return results.first['count']; + } +} + +class _CommentRepository extends _Repository { + _CommentRepository(DataClient client) : super(client); + + String get insertQuery => ''' + INSERT INTO + comment(id, workspaceId, cardId, userId, description) + VALUES(?1, ?2, ?3, ?4, ?5) + '''; + + Future createComment(Comment comment) async { + final results = + await client.getDBExecutor().execute('$insertQuery RETURNING *', [ + comment.id, + comment.workspaceId, + comment.cardId, + comment.userId, + comment.description + ]); + if (results.isEmpty) { + throw Exception("Failed to add Comment"); + } else { + return Comment.fromRow(results.first); + } + } + + String get updateQuery => ''' + UPDATE comment + set cardId = ?1, userId = ?2, description = ?3 + WHERE id = ?4 + '''; + + Future updateComment(Comment comment) async { + await client.getDBExecutor().execute(updateQuery, + [comment.cardId, comment.userId, comment.description, comment.id]); + return true; + } +} + +class _ListboardRepository extends _Repository { + _ListboardRepository(DataClient client) : super(client); + + Future> getListsByBoard({required String boardId}) async { + //first we get the listboards + final results = await client.getDBExecutor().execute(''' + SELECT * FROM listboard WHERE boardId = ? + ''', [boardId]); + List lists = + results.map((row) => Listboard.fromRow(row)).toList(); + + //then we set the cards for each listboard + for (Listboard list in lists) { + List cards = await client.card.getCardsforList(list.id); + list.cards = cards; + + for (Cardlist card in list.cards!) { + List labels = await client.cardLabel.getCardLabels(card); + card.cardLabels = labels; + } + } + + return lists; + } + + Stream> watchListsByBoard({required String boardId}) { + //first we get the listboards + return client.getDBExecutor().watch(''' + SELECT * FROM listboard WHERE boardId = ? ORDER BY listOrder ASC + ''', parameters: [boardId]).asyncMap((event) async { + List lists = + event.map((row) => Listboard.fromRow(row)).toList(); + + //then we set the cards for each listboard + for (Listboard list in lists) { + List cards = await client.card.getCardsforList(list.id); + list.cards = cards; + + for (Cardlist card in list.cards!) { + List labels = await client.cardLabel.getCardLabels(card); + card.cardLabels = labels; + } + } + + return lists; + }); + } + + String get insertQuery => ''' + INSERT INTO + listboard(id, workspaceId, boardId, userId, name, archived, listOrder) + VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7) + '''; + + Future createList(Listboard lst) async { + final results = + await client.getDBExecutor().execute('$insertQuery RETURNING *', [ + lst.id, + lst.workspaceId, + lst.boardId, + lst.userId, + lst.name, + boolAsInt(lst.archived), + lst.order + ]); + if (results.isEmpty) { + throw Exception("Failed to add Listboard"); + } else { + return Listboard.fromRow(results.first); + } + } + + String get updateQuery => ''' + UPDATE listboard + set listOrder = ?1 + WHERE id = ?2 + '''; + + Future updateListOrder(String listId, int newOrder) async { + await client.getDBExecutor().execute(updateQuery, [newOrder, listId]); + } + + /// Archive cards in and return how many were archived + /// This happens in a transaction + Future archiveCardsInList(Listboard list) async { + if (list.cards == null || list.cards!.isEmpty) { + return 0; + } + + //start transaction + return client.getDBExecutor().writeTransaction((sqlContext) async { + List cards = list.cards!; + int numCards = cards.length; + + //we set each of the cards in the list to archived = true + sqlContext.executeBatch(''' + UPDATE card + SET archived = 1 + WHERE id = ? + ''', cards.map((card) => [card.id]).toList()); + + //touch listboard to trigger update via stream listeners on Listboard + sqlContext.execute(''' + UPDATE listboard + SET archived = 0 + WHERE id = ? + ''', [list.id]); + + list.cards = []; + return numCards; + //end of transaction + }); + } +} + +class _MemberRepository extends _Repository { + _MemberRepository(DataClient client) : super(client); + + String get insertQuery => ''' + INSERT INTO + member(id, workspaceId, userId, name, role) + VALUES(?1, ?2, ?3, ?4, ?5) + '''; + + Future addMember(Member member) async { + final results = await client.getDBExecutor().execute( + '$insertQuery RETURNING *', [ + member.id, + member.workspaceId, + member.userId, + member.name, + member.role + ]); + if (results.isEmpty) { + throw Exception("Failed to add Member"); + } else { + return Member.fromRow(results.first); + } + } + + Future> getMembersByWorkspace( + {required String workspaceId}) async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM member WHERE workspaceId = ? + ''', [workspaceId]); + return results.map((row) => Member.fromRow(row)).toList(); + } + + Future> getInformationOfMembers(List members) async { + List users = []; + for (Member member in members) { + TrelloUser? user = await client.user.getUserById(userId: member.userId); + if (user != null) { + users.add(user); + } + } + return users; + } + + Future deleteMember(Member member, Workspace workspace) async { + //delete member + await client.getDBExecutor().execute( + 'DELETE FROM member WHERE workspaceId = ? AND id = ?', + [workspace.id, member.id]); + + //update workspace list with new members + List newMembersList = + await getMembersByWorkspace(workspaceId: workspace.id); + workspace.members = newMembersList; + return workspace; + } +} + +class _UserRepository extends _Repository { + _UserRepository(DataClient client) : super(client); + + String get insertQuery => ''' + INSERT INTO + trellouser(id, name, email, password) + VALUES(?1, ?2, ?3, ?4) + '''; + + Future createUser(TrelloUser user) async { + final results = await client.getDBExecutor().execute( + '$insertQuery RETURNING *', + [user.id, user.name, user.email, user.password]); + if (results.isEmpty) { + throw Exception("Failed to add User"); + } else { + return TrelloUser.fromRow(results.first); + } + } + + /// We excpect only one record in the local trellouser table + /// if somebody has logged in and not logged out again + Future getUser() async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM trellouser'''); + return results.map((row) => TrelloUser.fromRow(row)).firstOrNull; + } + + Future getUserById({required String userId}) async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM trellouser WHERE id = ? + ''', [userId]); + return results.map((row) => TrelloUser.fromRow(row)).firstOrNull; + } + + Future checkUserExists(String email) async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM trellouser WHERE email = ? + ''', [email]); + return results.map((row) => TrelloUser.fromRow(row)).firstOrNull; + } +} + +class _WorkspaceRepository extends _Repository { + _WorkspaceRepository(DataClient client) : super(client); + + String get insertQuery => ''' + INSERT INTO + workspace(id, userId, name, description, visibility) + VALUES(?1, ?2, ?3, ?4, ?5) + '''; + + Future createWorkspace(Workspace workspace) async { + final results = + await client.getDBExecutor().execute('$insertQuery RETURNING *', [ + workspace.id, + workspace.userId, + workspace.name, + workspace.description, + workspace.visibility + ]); + return Workspace.fromRow(results.first); + } + + Future> getWorkspacesByUser({required String userId}) async { + //First we get the workspaces + final results = await client.getDBExecutor().execute(''' + SELECT * FROM workspace WHERE userId = ? + ''', [userId]); + List workspaces = + results.map((row) => Workspace.fromRow(row)).toList(); + + //Then we get the members for each workspace + for (Workspace workspace in workspaces) { + List members = + await client.member.getMembersByWorkspace(workspaceId: workspace.id); + workspace.members = members; + } + + return workspaces; + } + + Stream> watchWorkspacesByUser({required String userId}) { + //First we get the workspaces + return client.getDBExecutor().watch( + ''' + SELECT * FROM workspace + ''', + ).asyncMap((event) async { + List workspaces = + event.map((row) => Workspace.fromRow(row)).toList(); + + //Then we get the members for each workspace + for (Workspace workspace in workspaces) { + List members = await client.member + .getMembersByWorkspace(workspaceId: workspace.id); + workspace.members = members; + } + return workspaces; + }); + } + + Future getWorkspaceById({required String workspaceId}) async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM workspace WHERE id = ? + ''', [workspaceId]); + Workspace workspace = Workspace.fromRow(results.first); + List members = + await client.member.getMembersByWorkspace(workspaceId: workspaceId); + workspace.members = members; + return workspace; + } + + Future> getBoardsByWorkspace( + {required String workspaceId}) async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM board WHERE workspaceId = ? + ''', [workspaceId]); + + List boards = results.map((row) => Board.fromRow(row)).toList(); + + for (Board board in boards) { + List labels = await client.boardLabel.getBoardLabels(board); + board.boardLabels = labels; + } + + return boards; + } + + Stream> watchBoardsByWorkspace({required String workspaceId}) { + return client.getDBExecutor().watch(''' + SELECT * FROM board WHERE workspaceId = ? + ''', parameters: [workspaceId]).asyncMap((event) async { + List boards = event.map((row) => Board.fromRow(row)).toList(); + + for (Board board in boards) { + List labels = await client.boardLabel.getBoardLabels(board); + board.boardLabels = labels; + } + + return boards; + }); + } + + Future updateWorkspace(Workspace workspace) async { + await client.getDBExecutor().execute(''' + UPDATE workspace + set userId = ?1, name = ?2, description = ?3, visibility = ?4 + WHERE id = ?5 + ''', [ + workspace.userId, + workspace.name, + workspace.description, + workspace.visibility, + workspace.id + ]); + return true; + } + + Future deleteWorkspace(Workspace workspace) async { + await client + .getDBExecutor() + .execute('DELETE FROM workspace WHERE id = ?', [workspace.id]); + return true; + } +} + +class _BoardLabelRepository extends _Repository { + _BoardLabelRepository(DataClient client) : super(client); + + Future createBoardLabel(BoardLabel boardLabel) async { + final results = await client.getDBExecutor().execute('''INSERT INTO + board_label(id, boardId, workspaceId, title, color, dateCreated) + VALUES(?, ?, ?, ?, ?, datetime()) + RETURNING *''', [ + boardLabel.id, + boardLabel.boardId, + boardLabel.workspaceId, + boardLabel.title, + boardLabel.color + ]); + if (results.isEmpty) { + throw Exception("Failed to add BoardLabel"); + } else { + return BoardLabel.fromRow(results.first); + } + } + + Future updateBoardLabel(BoardLabel boardLabel) async { + await client.getDBExecutor().execute(''' + UPDATE board_label + set title = ?1 + WHERE id = ?2 + ''', [boardLabel.title, boardLabel.id]); + return true; + } + + Future> getBoardLabels(Board board) async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM board_label WHERE boardId = ? ORDER BY dateCreated DESC + ''', [board.id]); + return results.map((row) => BoardLabel.fromRow(row)).toList(); + } +} + +class _CardLabelRepository extends _Repository { + _CardLabelRepository(DataClient client) : super(client); + + Future createCardLabel(CardLabel cardLabel) async { + final results = await client.getDBExecutor().execute('''INSERT INTO + card_label(id, cardId, boardId, workspaceId, boardLabelId, dateCreated) + VALUES(?, ?, ?, ?, ?, datetime()) + RETURNING *''', [ + cardLabel.id, + cardLabel.cardId, + cardLabel.boardId, + cardLabel.workspaceId, + cardLabel.boardLabelId, + ]); + if (results.isEmpty) { + throw Exception("Failed to add CardLabel"); + } else { + return CardLabel.fromRow(results.first); + } + } + + Future deleteCardLabel(BoardLabel boardLabel) async { + await client.getDBExecutor().execute( + 'DELETE FROM card_label WHERE boardLabelId = ?', [boardLabel.id]); + return true; + } + + Future> getCardLabels(Cardlist card) async { + final results = await client.getDBExecutor().execute(''' + SELECT * FROM card_label WHERE cardId = ? ORDER BY dateCreated DESC + ''', [card.id]); + return results.map((row) => CardLabel.fromRow(row)).toList(); + } +} + +class DataClient { + late final _ActivityRepository activity; + late final _AttachmentRepository attachment; + late final _BoardRepository board; + late final _CardlistRepository card; + late final _CheckListRepository checklist; + late final _CommentRepository comment; + late final _ListboardRepository listboard; + late final _MemberRepository member; + late final _UserRepository user; + late final _WorkspaceRepository workspace; + late final _BoardLabelRepository boardLabel; + late final _CardLabelRepository cardLabel; + + late PowerSyncClient _powerSyncClient; + + DataClient() { + activity = _ActivityRepository(this); + attachment = _AttachmentRepository(this); + board = _BoardRepository(this); + card = _CardlistRepository(this); + checklist = _CheckListRepository(this); + comment = _CommentRepository(this); + listboard = _ListboardRepository(this); + member = _MemberRepository(this); + user = _UserRepository(this); + workspace = _WorkspaceRepository(this); + boardLabel = _BoardLabelRepository(this); + cardLabel = _CardLabelRepository(this); + } + + Future initialize() async { + _powerSyncClient = PowerSyncClient(); + await _powerSyncClient.initialize(); + } + + PowerSyncDatabase getDBExecutor() { + return _powerSyncClient.getDBExecutor(); + } + + bool isLoggedIn() { + return _powerSyncClient.isLoggedIn(); + } + + String? getUserId() { + return _powerSyncClient.getUserId(); + } + + Future getLoggedInUser() async { + String? userId = _powerSyncClient.getUserId(); + if (userId != null) { + return user.getUserById(userId: userId); + } else + return null; + } + + Future logOut() async { + await _powerSyncClient.logout(); + } + + Future loginWithEmail(String email, String password) async { + String userId = await _powerSyncClient.loginWithEmail(email, password); + + TrelloUser? storedUser = await user.getUserById(userId: userId); + if (storedUser == null) { + storedUser = await user.createUser(TrelloUser( + id: userId, + name: email.split('@')[0], + email: email, + password: password)); + } + return storedUser; + } + + Future signupWithEmail( + String name, String email, String password) async { + TrelloUser? storedUser = await user.checkUserExists(email); + if (storedUser != null) { + throw new Exception('User for email already exists. Use Login instead.'); + } + return _powerSyncClient.signupWithEmail(name, email, password); + } + + SyncStatus getCurrentSyncStatus() { + return _powerSyncClient.currentStatus; + } + + Stream getStatusStream() { + return _powerSyncClient.statusStream; + } + + Future switchToOfflineMode() async { + await _powerSyncClient.switchToOfflineMode(); + } + + Future switchToOnlineMode() async { + await _powerSyncClient.switchToOnlineMode(); + } +} diff --git a/demos/supabase-trello/lib/protocol/powersync.dart b/demos/supabase-trello/lib/protocol/powersync.dart new file mode 100644 index 00000000..af7d0a56 --- /dev/null +++ b/demos/supabase-trello/lib/protocol/powersync.dart @@ -0,0 +1,269 @@ +// This file performs setup of the PowerSync database +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:powersync/powersync.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:trelloappclone_flutter/models/user.dart'; + +import '../models/schema.dart'; + +final log = Logger('powersync-supabase'); + +/// Postgres Response codes that we cannot recover from by retrying. +final List fatalResponseCodes = [ + // Class 22 — Data Exception + // Examples include data type mismatch. + RegExp(r'^22...$'), + // Class 23 — Integrity Constraint Violation. + // Examples include NOT NULL, FOREIGN KEY and UNIQUE violations. + RegExp(r'^23...$'), + // INSUFFICIENT PRIVILEGE - typically a row-level security violation + RegExp(r'^42501$'), +]; + +/// Use Supabase for authentication and data upload. +class SupabaseConnector extends PowerSyncBackendConnector { + PowerSyncDatabase db; + + Future? _refreshFuture; + + SupabaseConnector(this.db); + + /// Get a Supabase token to authenticate against the PowerSync instance. + @override + Future fetchCredentials() async { + // Wait for pending session refresh if any + await _refreshFuture; + + // Use Supabase token for PowerSync + final existingSession = Supabase.instance.client.auth.currentSession; + if (existingSession?.accessToken == null) { + // Not logged in + return null; + } + + // Force session refresh. + final authResponse = await Supabase.instance.client.auth.refreshSession(); + final session = authResponse.session; + if (session == null) { + // Probably shouldn't happen + return null; + } + + // Use the access token to authenticate against PowerSync + final token = session.accessToken; + + // userId and expiresAt are for debugging purposes only + final userId = session.user.id; + final expiresAt = session.expiresAt == null + ? null + : DateTime.fromMillisecondsSinceEpoch(session.expiresAt! * 1000); + return PowerSyncCredentials( + endpoint: dotenv.env['POWERSYNC_URL']!, + token: token, + userId: userId, + expiresAt: expiresAt); + } + + @override + void invalidateCredentials() { + // Trigger a session refresh if auth fails on PowerSync. + // Generally, sessions should be refreshed automatically by Supabase. + // However, in some cases it can be a while before the session refresh is + // retried. We attempt to trigger the refresh as soon as we get an auth + // failure on PowerSync. + // + // This could happen if the device was offline for a while and the session + // expired, and nothing else attempt to use the session it in the meantime. + // + // Timeout the refresh call to avoid waiting for long retries, + // and ignore any errors. Errors will surface as expired tokens. + _refreshFuture = Supabase.instance.client.auth + .refreshSession() + .timeout(const Duration(seconds: 5)) + .then((response) => null, onError: (error) => null); + } + + // Upload pending changes to Supabase. + @override + Future uploadData(PowerSyncDatabase database) async { + // This function is called whenever there is data to upload, whether the + // device is online or offline. + // If this call throws an error, it is retried periodically. + final transaction = await database.getNextCrudTransaction(); + if (transaction == null) { + return; + } + + final rest = Supabase.instance.client.rest; + CrudEntry? lastOp; + try { + // Note: If transactional consistency is important, use database functions + // or edge functions to process the entire transaction in a single call. + for (var op in transaction.crud) { + lastOp = op; + + final table = rest.from(op.table); + if (op.op == UpdateType.put) { + var data = Map.of(op.opData!); + data['id'] = op.id; + await table.upsert(data); + } else if (op.op == UpdateType.patch) { + await table.update(op.opData!).eq('id', op.id); + } else if (op.op == UpdateType.delete) { + await table.delete().eq('id', op.id); + } + } + + // All operations successful. + await transaction.complete(); + } on PostgrestException catch (e) { + if (e.code != null && + fatalResponseCodes.any((re) => re.hasMatch(e.code!))) { + /// Instead of blocking the queue with these errors, + /// discard the (rest of the) transaction. + /// + /// Note that these errors typically indicate a bug in the application. + /// If protecting against data loss is important, save the failing records + /// elsewhere instead of discarding, and/or notify the user. + log.severe('Data upload error - discarding $lastOp', e); + await transaction.complete(); + } else { + // Error may be retryable - e.g. network error or temporary server error. + // Throwing an error here causes this call to be retried after a delay. + rethrow; + } + } + } +} + +class PowerSyncClient { + PowerSyncClient._(); + + static final PowerSyncClient _instance = PowerSyncClient._(); + + bool _isInitialized = false; + bool _offlineMode = false; + + late final PowerSyncDatabase _db; + + factory PowerSyncClient() { + return _instance; + } + + Future initialize({bool offlineMode = false}) async { + if (!_isInitialized) { + _offlineMode = offlineMode; + await dotenv.load(fileName: '.env'); + await _openDatabase(); + _isInitialized = true; + } + } + + PowerSyncDatabase getDBExecutor() { + if (!_isInitialized) { + throw Exception( + 'PowerSyncDatabase not initialized. Call initialize() first.'); + } + return _db; + } + + bool isLoggedIn() { + return Supabase.instance.client.auth.currentSession?.accessToken != null; + } + + /// id of the user currently logged in + String? getUserId() { + return Supabase.instance.client.auth.currentSession?.user.id; + } + + Future getDatabasePath() async { + final dir = await getApplicationSupportDirectory(); + return join(dir.path, 'powersync-trello-demo.db'); + } + + _loadSupabase() async { + await Supabase.initialize( + url: dotenv.env['SUPABASE_URL']!, + anonKey: dotenv.env['SUPABASE_ANON_KEY']!, + ); + } + + Future _openDatabase() async { + // Open the local database + _db = PowerSyncDatabase(schema: schema, path: await getDatabasePath()); + await _db.initialize(); + + await _loadSupabase(); + + if (isLoggedIn() && !_offlineMode) { + // If the user is already logged in, connect immediately. + // Otherwise, connect once logged in. + _db.connect(connector: SupabaseConnector(_db)); + } + + Supabase.instance.client.auth.onAuthStateChange.listen((data) async { + final AuthChangeEvent event = data.event; + if (event == AuthChangeEvent.signedIn) { + // Connect to PowerSync when the user is signed in + _db.connect(connector: SupabaseConnector(_db)); + } else if (event == AuthChangeEvent.signedOut) { + // Implicit sign out - disconnect, but don't delete data + await _db.disconnect(); + } + }); + } + + bool isOfflineMode() { + return _offlineMode; + } + + Future switchToOfflineMode() async { + log.info('Switching to Offline mode'); + _offlineMode = true; + await _db.disconnect(); + } + + Future switchToOnlineMode() async { + log.info('Switching to Online mode'); + _offlineMode = false; + if (isLoggedIn()) { + _db.connect(connector: SupabaseConnector(_db)); + } + } + + Future signupWithEmail( + String name, String email, String password) async { + AuthResponse authResponse = await Supabase.instance.client.auth + .signUp(email: email, password: password); + + return TrelloUser( + id: authResponse.user!.id, + name: name, + email: email, + password: password); + } + + Future loginWithEmail(String email, String password) async { + AuthResponse authResponse = await Supabase.instance.client.auth + .signInWithPassword(email: email, password: password); + + return authResponse.user!.id; + } + + /// Explicit sign out - clear database and log out. + Future logout() async { + if (!_isInitialized) { + throw Exception( + 'PowerSyncClient not initialized. Call initialize() first.'); + } + await Supabase.instance.client.auth.signOut(); + await _db.disconnectAndClear(); + } + + /// Getting current connection status + SyncStatus get currentStatus => _db.currentStatus; + Stream get statusStream => _db.statusStream; +} diff --git a/demos/supabase-trello/lib/utils/color.dart b/demos/supabase-trello/lib/utils/color.dart new file mode 100644 index 00000000..d4ea5f21 --- /dev/null +++ b/demos/supabase-trello/lib/utils/color.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; + +const brandColor = Color(0xff38b6ff); +const whiteShade = Color(0xfff0f0f0); +const dangerColor = Color(0xff800200); +const themeColor = Color(0xff02457a); diff --git a/demos/supabase-trello/lib/utils/config.dart b/demos/supabase-trello/lib/utils/config.dart new file mode 100644 index 00000000..7b10fea7 --- /dev/null +++ b/demos/supabase-trello/lib/utils/config.dart @@ -0,0 +1,44 @@ +import 'package:flutter/painting.dart'; + +const logo = "assets/trello-logo.png"; +const landingImage = "assets/landing.jpg"; +const backgrounds = [ + "#ADD8E6" + "#89CFF0", + "#0000FF", + "#7393B3", + "#088F8F", + "#0096FF", + "#5F9EA0", + "#0047AB", + "#6495ED", + "#00FFFF", + "#00008B", + "#6F8FAF", + "#1434A4", + "#7DF9FF", + "#6082B6", + "#00A36C", + "#3F00FF", + "#5D3FD3" +]; + +const listMenu = [ + "Add Card", + "Copy list", + "Move list", + "Watch", + "Sort by", + "Move all cards in this list", + "Archive all cards in this list", + "Archive list" +]; + +const labels = [ + Color(0xffADD8E6), + Color(0xff89CFF0), + Color(0xff0000FF), + Color(0xff7393B3) +]; + +enum Sign { signUp, logIn } diff --git a/demos/supabase-trello/lib/utils/constant.dart b/demos/supabase-trello/lib/utils/constant.dart new file mode 100644 index 00000000..7c04692f --- /dev/null +++ b/demos/supabase-trello/lib/utils/constant.dart @@ -0,0 +1,40 @@ +const headline = "Move teamwork forward - even on the go"; +const terms = + "By signing up, you agree to our Terms of service and Privacy Policy"; +const contact = "Contact support"; +const visibilityConfigurations = [ + { + "type": "Private", + "description": + "The board is private. Only people added to the board can view and edit it" + }, + {"type": "Workspace", "description": "Anyone at BOARDNAME can see this file"}, + { + "type": "Public", + "description": + "The board is public. It's visible to anyone with the link and will show up in search engines like Google. Only people added to the board can edit it" + }, +]; +const defaultDescription = + "It's your board's time to shine! Let people know what this board is used for and what they can expect to see"; +const powerups = [ + { + "title": "Calendar Power-Up", + "description": + "See your work over time! The Trello Calendar Power-Up displays all cards with due dates by month" + }, + { + "title": "Card Aging", + "description": + "Quickly visualize inactive cards on your board, and keep tasks from going incomplete" + }, + { + "title": "Voting", + "description": "Give power to the people, and allow users to vote on cards" + }, + { + "title": "List limits", + "description": + "Set a limit on your lists and we'll highlight the list if the number of cards in it passes the limit" + } +]; \ No newline at end of file diff --git a/demos/supabase-trello/lib/utils/data_generator.dart b/demos/supabase-trello/lib/utils/data_generator.dart new file mode 100644 index 00000000..9c821fbd --- /dev/null +++ b/demos/supabase-trello/lib/utils/data_generator.dart @@ -0,0 +1,123 @@ +import 'dart:math'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:random_name_generator/random_name_generator.dart'; +import 'package:status_alert/status_alert.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; +import 'package:trelloappclone_flutter/utils/config.dart'; +import 'package:trelloappclone_flutter/utils/service.dart'; +import 'package:trelloappclone_flutter/utils/trello_provider.dart'; +import 'package:trelloappclone_flutter/models/listboard.dart'; +import 'package:trelloappclone_flutter/models/board.dart'; +import 'package:trelloappclone_flutter/models/workspace.dart'; +import 'package:trelloappclone_flutter/models/card.dart'; +import 'package:trelloappclone_flutter/models/checklist.dart'; +import 'package:trelloappclone_flutter/models/comment.dart'; + +/// This class generates an example workspace with some boards, cards, etc for each +class DataGenerator with Service { + Random random = Random(); + var randomNames = RandomNames(Zone.us); + + List _generateBoardNames(String workspaceName) { + String prepend = workspaceName.splitMapJoin(' ', + onMatch: (match) => '', + onNonMatch: (text) => text.substring(0, 1).toUpperCase()); + return [ + '$prepend MVP App', + '$prepend User Auth Service', + '$prepend Transactions Service', + '$prepend Reporting Service', + '$prepend DevOps' + ]; + } + + List _generateListNames() { + return ['To Do', 'In Progress', 'To Test', 'Testing', 'To Release', 'Done']; + } + + List _generateCardNames(String prepend, int nrOfCards) { + return List.generate(nrOfCards, (index) => '$prepend Card $index'); + } + + createSampleWorkspace( + String workspaceName, TrelloProvider trello, BuildContext context) async { + StatusAlert.show(context, + duration: const Duration(seconds: 3), + title: 'Generating Workspace Data...', + configuration: + const IconConfiguration(icon: Icons.sync, color: brandColor), + maxWidth: 260); + + Workspace workspace = await createWorkspace(context, + name: workspaceName, + description: 'Example workspace', + visibility: 'Public'); + for (String boardName in _generateBoardNames(workspaceName)) { + //create board + Board newBoard = Board( + id: randomUuid(), + workspaceId: workspace.id, + userId: trello.user.id, + name: boardName, + visibility: 'Workspace', + background: backgrounds[random.nextInt(16)]); + await createBoard(context, newBoard); + + int listIndex = 1; + //create lists for each board + for (String listName in _generateListNames()) { + Listboard newList = Listboard( + id: randomUuid(), + workspaceId: workspace.id, + boardId: newBoard.id, + userId: trello.user.id, + name: listName, + order: listIndex++); + await addList(newList); + + //create cards per list + int cardIndex = 1; + for (String cardName + in _generateCardNames(listName, random.nextInt(10))) { + Cardlist newCard = Cardlist( + id: randomUuid(), + workspaceId: workspace.id, + listId: newList.id, + userId: trello.user.id, + name: cardName, + rank: cardIndex++); + await addCard(newCard); + await createComment(Comment( + id: randomUuid(), + workspaceId: workspace.id, + cardId: newCard.id, + userId: trello.user.id, + description: '${randomNames.name()} said something interesting', + )); + await createChecklist(Checklist( + id: randomUuid(), + workspaceId: workspace.id, + cardId: newCard.id, + name: '${randomNames.name()} need to check this', + status: false)); + await createChecklist(Checklist( + id: randomUuid(), + workspaceId: workspace.id, + cardId: newCard.id, + name: '${randomNames.name()} need to check this', + status: false)); + await createActivity( + workspaceId: workspace.id, + boardId: newBoard.id, + card: newCard.id, + description: '${randomNames.name()} updated this card'); + } + ; + } + ; + } + ; + } +} diff --git a/demos/supabase-trello/lib/utils/service.dart b/demos/supabase-trello/lib/utils/service.dart new file mode 100644 index 00000000..d9727abc --- /dev/null +++ b/demos/supabase-trello/lib/utils/service.dart @@ -0,0 +1,465 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:crypto/crypto.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart' hide Card; +import 'package:status_alert/status_alert.dart'; +import 'package:trelloappclone_flutter/features/home/presentation/custom_search.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; +import 'package:trelloappclone_flutter/models/listboard.dart'; +import 'package:trelloappclone_flutter/models/board.dart'; +import 'package:trelloappclone_flutter/models/workspace.dart'; +import 'package:trelloappclone_flutter/models/user.dart'; +import 'package:trelloappclone_flutter/models/card.dart'; +import 'package:trelloappclone_flutter/models/member.dart'; +import 'package:trelloappclone_flutter/models/checklist.dart'; +import 'package:trelloappclone_flutter/models/comment.dart'; +import 'package:trelloappclone_flutter/models/activity.dart'; +import 'package:trelloappclone_flutter/models/board_label.dart'; +import 'package:trelloappclone_flutter/models/card_label.dart'; +import 'package:trelloappclone_flutter/models/attachment.dart'; + +import 'package:uuid/uuid.dart'; + +import '../main.dart'; + +var uuid = Uuid(); + +mixin Service { + randomUuid() { + return uuid.v4(); + } + + //sign up new user + signUp( + {required String name, + required String email, + required String password, + required BuildContext context}) async { + try { + TrelloUser user = await dataClient.signupWithEmail(name, email, password); + await dataClient.user.createUser(user); + + if (context.mounted) { + Navigator.pushNamed(context, '/'); + StatusAlert.show(context, + duration: const Duration(seconds: 3), + title: 'Account Created', + subtitle: 'Log in with your new credentials', + subtitleOptions: StatusAlertTextConfiguration( + softWrap: true, maxLines: 3, overflow: TextOverflow.ellipsis), + configuration: + const IconConfiguration(icon: Icons.check, color: brandColor), + maxWidth: 260); + } + } on Exception catch (e) { + log('Error with signup: $e', error: e); + StatusAlert.show(context, + duration: const Duration(seconds: 5), + title: 'Sign Up Error', + subtitle: e.toString(), + subtitleOptions: StatusAlertTextConfiguration( + softWrap: true, maxLines: 3, overflow: TextOverflow.ellipsis), + configuration: + const IconConfiguration(icon: Icons.check, color: brandColor), + maxWidth: 260); + } + } + + //log in existing user + logIn(String email, String password, BuildContext context) async { + try { + TrelloUser user = await dataClient.loginWithEmail(email, password); + trello.setUser(user); + + if (context.mounted) { + Navigator.pushNamed(context, '/home'); + StatusAlert.show(context, + duration: const Duration(seconds: 5), + title: 'Syncing Workspaces...', + configuration: + const IconConfiguration(icon: Icons.sync, color: brandColor), + maxWidth: 260); + } + } on Exception catch (e) { + log('Error with login: $e', error: e); + StatusAlert.show(context, + duration: const Duration(seconds: 5), + title: 'Login Error', + subtitle: e.toString(), + subtitleOptions: StatusAlertTextConfiguration( + softWrap: true, maxLines: 3, overflow: TextOverflow.ellipsis), + configuration: + const IconConfiguration(icon: Icons.check, color: brandColor), + maxWidth: 260); + } + } + + //log out user + logOut(BuildContext context) async { + try { + await dataClient.logOut(); + } on Exception catch (e) { + StatusAlert.show(context, + duration: const Duration(seconds: 5), + title: 'Logout Error', + subtitle: e.toString(), + subtitleOptions: StatusAlertTextConfiguration( + softWrap: true, maxLines: 3, overflow: TextOverflow.ellipsis), + configuration: + const IconConfiguration(icon: Icons.check, color: brandColor), + maxWidth: 260); + } + } + + switchToOfflineMode() async { + dataClient.switchToOfflineMode(); + } + + switchToOnlineMode() async { + dataClient.switchToOnlineMode(); + } + + //search for a board + search(BuildContext context) async { + List allboards = await dataClient.board.getAllBoards(); + + if (context.mounted) { + showSearch(context: context, delegate: CustomSearchDelegate(allboards)); + } + } + + //encrypt password + String encryptPassword(String password) { + final bytes = utf8.encode(password); + final hash = sha256.convert(bytes); + return hash.toString(); + } + + //create workspace + createWorkspace(BuildContext context, + {required String name, + required String description, + required String visibility}) async { + Workspace workspace = Workspace( + id: randomUuid(), + userId: trello.user.id, + name: name, + description: description, + visibility: visibility); + + try { + Workspace addedWorkspace = + await dataClient.workspace.createWorkspace(workspace); + + Member newMember = Member( + id: randomUuid(), + workspaceId: addedWorkspace.id, + userId: trello.user.id, + name: trello.user.name ?? trello.user.email, + role: "Admin"); + + await dataClient.member.addMember(newMember); + + if (context.mounted) { + Navigator.pushNamed(context, "/home"); + } + + return addedWorkspace; + } on Exception catch (e) { + StatusAlert.show(context, + duration: const Duration(seconds: 5), + title: 'Trello Clone', + subtitle: e.toString(), + configuration: + const IconConfiguration(icon: Icons.check, color: brandColor), + maxWidth: 260); + } + } + + //get workspaces of a specific user using user ID + Future> getWorkspaces() async { + List workspaces = + await dataClient.workspace.getWorkspacesByUser(userId: trello.user.id); + trello.setWorkspaces(workspaces); + return workspaces; + } + + //get a stream of workspaces for user, so we can react on distributed changes to it + Stream> getWorkspacesStream() { + return dataClient.workspace + .watchWorkspacesByUser(userId: trello.user.id) + .map((workspaces) { + trello.setWorkspaces(workspaces); + return workspaces; + }); + } + + //create board + createBoard(BuildContext context, Board brd) async { + try { + var labelColors = [{}]; + labelColors = [ + {"color": "fd6267", "name": "Bug"}, + {"color": "67ddf3", "name": "Feature"}, + {"color": "a282ff", "name": "Enhancement"}, + {"color": "f7b94a", "name": "Documentation"}, + {"color": "f8ff6e", "name": "Marketing"} + ]; + + await dataClient.board.createBoard(brd); + // Add the default labels to the board + for (var label in labelColors) { + await dataClient.boardLabel.createBoardLabel(BoardLabel( + id: randomUuid(), + boardId: brd.id, + workspaceId: brd.workspaceId, + title: label["name"] ?? "", + color: label["color"] ?? "", + dateCreated: DateTime.now())); + } + + if (context.mounted) { + Navigator.pushNamed(context, "/home"); + } + } on Exception catch (e) { + StatusAlert.show(context, + duration: const Duration(seconds: 5), + title: 'Trello Clone', + subtitle: e.toString(), + configuration: + const IconConfiguration(icon: Icons.check, color: brandColor), + maxWidth: 260); + } + } + + //get boards of a specific workspace by Workspace ID + Future> getBoards(String workspaceId) async { + List boards = await dataClient.workspace + .getBoardsByWorkspace(workspaceId: workspaceId); + trello.setBoards(boards); + return boards; + } + + //watch boards of a specific workspace by Workspace ID via a stream + Stream> getBoardsStream(String workspaceId) { + return dataClient.workspace + .watchBoardsByWorkspace(workspaceId: workspaceId) + .map((boards) { + trello.setBoards(boards); + return boards; + }); + } + + //update workspace + Future updateWorkspace(Workspace wkspc) async { + return await dataClient.workspace.updateWorkspace(wkspc); + } + + //get user by Id + Future getUserById(String userId) async { + TrelloUser? user = await dataClient.user.getUserById(userId: userId); + return user; + } + + //get information of members + Future> getMembersInformation(List mmbrs) async { + List usrs = + await dataClient.member.getInformationOfMembers(mmbrs); + return usrs; + } + + Future inviteUserToWorkspace(String email, Workspace workspace) async { + TrelloUser? user = await dataClient.user.checkUserExists(email); + if (user != null) { + Member member = Member( + id: randomUuid(), + workspaceId: workspace.id, + userId: user.id, + name: user.name ?? user.email, + role: "Admin"); + await dataClient.member.addMember(member); + workspace.members?.add(member); + return true; + } + return false; + } + + //remove Member from Workspace + Future removeMemberFromWorkspace( + Member mmbr, Workspace wkspc) async { + Workspace updatedWorkspace = + await dataClient.member.deleteMember(mmbr, wkspc); + return updatedWorkspace; + } + + //update offline status + Future updateOfflineStatus(Board brd) async { + return await dataClient.board.updateBoard(brd); + } + + //get lists by board + Future> getListsByBoard(Board brd) async { + List brdlist = + await dataClient.listboard.getListsByBoard(boardId: brd.id); + trello.setListBoard(brdlist); + return brdlist; + } + + //watch lists by board via Stream + Stream> getListsByBoardStream(Board brd) { + return dataClient.listboard.watchListsByBoard(boardId: brd.id).map((lists) { + trello.setListBoard(lists); + return lists; + }); + } + + //add list + Future addList(Listboard lst) async { + await dataClient.listboard.createList(lst); + createActivity( + workspaceId: lst.workspaceId, + description: "${trello.user.name} added a new list ${lst.name}"); + } + + //add list + Future updateListOrder(String listId, int newOrder) async { + await dataClient.listboard.updateListOrder(listId, newOrder); + } + + //add card + Future addCard(Cardlist crd) async { + Cardlist newcrd = await dataClient.card.createCard(crd); + createActivity( + card: newcrd.id, + workspaceId: newcrd.workspaceId, + description: "${trello.user.name} added a new card ${crd.name}"); + } + + //update card + Future updateCard(Cardlist crd) async { + await dataClient.card.updateCard(crd); + + createActivity( + card: crd.id, + workspaceId: crd.workspaceId, + description: "${trello.user.name} updated the card ${crd.name}"); + } + + //delete card + Future deleteCard(Cardlist crd) async { + await dataClient.card.deleteCard(crd); + + createActivity( + card: crd.id, + workspaceId: crd.workspaceId, + description: "${trello.user.name} deleted the card ${crd.name}"); + } + + Future archiveCardsInList(Listboard list) async { + return dataClient.listboard.archiveCardsInList(list); + } + + //add card + Future addCardLabel(CardLabel crdlbl, BoardLabel brdlbl) async { + final newCardLabel = await dataClient.cardLabel.createCardLabel(crdlbl); + createActivity( + card: crdlbl.cardId, + workspaceId: brdlbl.workspaceId, + description: "${trello.user.name} added a new label '${brdlbl.title}'"); + + return newCardLabel; + } + + //add card + Future deleteCardLabel(cardId, BoardLabel brdlbl) async { + await dataClient.cardLabel.deleteCardLabel(brdlbl); + createActivity( + card: cardId, + workspaceId: brdlbl.workspaceId, + description: "${trello.user.name} deleted the label '${brdlbl.title}'"); + } + + //updateBoardLabel + Future updateBoardLabel(BoardLabel brdlbl) async { + await dataClient.boardLabel.updateBoardLabel(brdlbl); + } + + //create activity + Future createActivity( + {required String workspaceId, + String? boardId, + required String description, + String? card}) async { + await dataClient.activity.createActivity(Activity( + id: randomUuid(), + workspaceId: workspaceId, + boardId: boardId, + userId: trello.user.id, + cardId: card, + description: description, + dateCreated: DateTime.now())); + } + + //get activities of a specific card + Future> getActivities(Cardlist crd) async { + return dataClient.activity.getActivities(crd); + } + + //create comment + Future createComment(Comment cmmt) async { + await dataClient.comment.createComment(cmmt); + } + + //create checklist + Future createChecklist(Checklist chcklst) async { + await dataClient.checklist.createChecklist(chcklst); + } + + Future> getChecklists(Cardlist crd) async { + List chcklsts = await dataClient.checklist.getChecklists(crd); + return chcklsts; + } + + Future updateChecklist(Checklist chcklst) async { + await dataClient.checklist.updateChecklist(chcklst); + } + + Future deleteChecklist(Cardlist crd) async { + await dataClient.checklist.deleteChecklist(crd); + } + + Future uploadFile(Cardlist crd) async { + FilePickerResult? result = + await FilePicker.platform.pickFiles(allowMultiple: false); + if (result != null) { + addAttachment(result.files[0].path ?? "", crd); + } + } + + Future addAttachment(String path, Cardlist crd) async { + //TODO: fix uploads + // var uploadDescription = await client.attachment.getUploadDescription(path); + bool success = false; + // if (uploadDescription != null) { + // var uploader = FileUploader(uploadDescription); + // await uploader.upload( + // File(path).readAsBytes().asStream(), File(path).lengthSync()); + // success = await client.attachment.verifyUpload(path); + // } + // if (success) { + // insertAttachment(crd, path); + // } + return success; + } + + Future insertAttachment(Cardlist crd, String path) async { + await dataClient.attachment.addAttachment(Attachment( + id: randomUuid(), + workspaceId: crd.workspaceId, + userId: trello.user.id, + cardId: crd.id, + attachment: path)); + } +} diff --git a/demos/supabase-trello/lib/utils/trello_provider.dart b/demos/supabase-trello/lib/utils/trello_provider.dart new file mode 100644 index 00000000..16830e39 --- /dev/null +++ b/demos/supabase-trello/lib/utils/trello_provider.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/models/listboard.dart'; +import 'package:trelloappclone_flutter/models/board.dart'; +import 'package:trelloappclone_flutter/models/workspace.dart'; +import 'package:trelloappclone_flutter/models/user.dart'; +import 'package:trelloappclone_flutter/models/card.dart'; + +import 'config.dart'; + +class TrelloProvider extends ChangeNotifier { + late TrelloUser _user; + TrelloUser get user => _user; + + List _workspaces = []; + List get workspaces => _workspaces; + + List _boards = []; + List get boards => _boards; + + String _selectedBackground = backgrounds[0]; + String get selectedBackground => _selectedBackground; + + List _lstbrd = []; + List get lstbrd => _lstbrd; + + late Board _selectedBoard; + Board get selectedBoard => _selectedBoard; + + late Workspace _selectedWorkspace; + Workspace get selectedWorkspace => _selectedWorkspace; + + Cardlist? _selectedCard; + Cardlist? get selectedCard => _selectedCard; + + void setUser(TrelloUser user) { + _user = user; + notifyListeners(); + } + + void setWorkspaces(List wkspcs) { + _workspaces = wkspcs; + notifyListeners(); + } + + void setBoards(List brd) { + _boards = brd; + notifyListeners(); + } + + void setSelectedBg(String slctbg) { + _selectedBackground = slctbg; + notifyListeners(); + } + + void setListBoard(List lstbrd) { + _lstbrd = lstbrd; + notifyListeners(); + } + + void setSelectedBoard(Board brd) { + _selectedBoard = brd; + notifyListeners(); + } + + void setSelectedWorkspace(Workspace workspace) { + _selectedWorkspace = workspace; + notifyListeners(); + } + + void setSelectedCard(Cardlist? card) { + _selectedCard = card; + notifyListeners(); + } +} diff --git a/demos/supabase-trello/lib/utils/widgets.dart b/demos/supabase-trello/lib/utils/widgets.dart new file mode 100644 index 00000000..4d0e45a8 --- /dev/null +++ b/demos/supabase-trello/lib/utils/widgets.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:trelloappclone_flutter/utils/color.dart'; + +class LabelDiplay extends StatefulWidget { + final String label; + final String color; + const LabelDiplay({required this.label, required this.color, super.key}); + + @override + State createState() => _LabelDiplayState(); +} + +class _LabelDiplayState extends State { + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + color: Color(int.parse(widget.color, radix: 16) + 0xFF000000), + borderRadius: const BorderRadius.all(Radius.circular(5.0))), + child: Center( + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Text( + widget.label, + style: TextStyle( + color: ((int.parse(widget.color, radix: 16)) > 186 + ? Colors.black + : Colors.white)), + ), + ), + )); + } +} + +class ColorSquare extends StatefulWidget { + final String bckgrd; + const ColorSquare({required this.bckgrd, super.key}); + + @override + State createState() => _ColorSquareState(); +} + +class _ColorSquareState extends State { + @override + Widget build(BuildContext context) { + return Container( + height: 40.0, + width: 40.0, + decoration: BoxDecoration( + color: Color( + int.parse(widget.bckgrd.substring(1, 7), radix: 16) + 0xFF000000), + borderRadius: const BorderRadius.all(Radius.circular(5.0))), + ); + } +} + +class BlueRectangle extends StatefulWidget { + const BlueRectangle({super.key}); + + @override + State createState() => _BlueRectangleState(); +} + +class _BlueRectangleState extends State { + @override + Widget build(BuildContext context) { + return Container( + height: 100.0, + width: 300.0, + decoration: const BoxDecoration( + color: brandColor, + borderRadius: BorderRadius.all(Radius.circular(5.0))), + ); + } +} diff --git a/demos/supabase-trello/lib/widgets/thirdparty/README.md b/demos/supabase-trello/lib/widgets/thirdparty/README.md new file mode 100644 index 00000000..c1a4c771 --- /dev/null +++ b/demos/supabase-trello/lib/widgets/thirdparty/README.md @@ -0,0 +1,5 @@ +# BoardView + +The code for this BoardView widget comes from this repo: https://github.com/jakebonk/FlutterBoardView + +However, since that project is not being maintained anymore, and somehow the latest code in the `master` repo does not match the actual lib code from pub.dev, we pulled in the code directly in this project in order to make it compilable again with latest Flutter version. \ No newline at end of file diff --git a/demos/supabase-trello/lib/widgets/thirdparty/board_item.dart b/demos/supabase-trello/lib/widgets/thirdparty/board_item.dart new file mode 100644 index 00000000..da106d62 --- /dev/null +++ b/demos/supabase-trello/lib/widgets/thirdparty/board_item.dart @@ -0,0 +1,150 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +import 'board_list.dart'; + +typedef void OnDropItem(int? listIndex, int? itemIndex, int? oldListIndex, + int? oldItemIndex, BoardItemState state); +typedef void OnTapItem(int listIndex, int itemIndex, BoardItemState state); +typedef void OnStartDragItem( + int? listIndex, int? itemIndex, BoardItemState state); +typedef void OnDragItem(int oldListIndex, int oldItemIndex, int newListIndex, + int newItemIndex, BoardItemState state); + +class BoardItem extends StatefulWidget { + final BoardListState? boardList; + final Widget? item; + final int? index; + final OnDropItem? onDropItem; + final OnTapItem? onTapItem; + final OnStartDragItem? onStartDragItem; + final OnDragItem? onDragItem; + final bool draggable; + + const BoardItem( + {Key? key, + this.boardList, + this.item, + this.index, + this.onDropItem, + this.onTapItem, + this.onStartDragItem, + this.draggable = true, + this.onDragItem}) + : super(key: key); + + @override + State createState() { + return BoardItemState(); + } +} + +class BoardItemState extends State + with AutomaticKeepAliveClientMixin { + late double height; + double? width; + + @override + bool get wantKeepAlive => true; + + void onDropItem(int? listIndex, int? itemIndex) { + if (widget.onDropItem != null) { + // check if itemIndex == items.length + // log everything + widget.onDropItem!( + listIndex, + itemIndex, + widget.boardList!.widget.boardView!.startListIndex, + widget.boardList!.widget.boardView!.startItemIndex, + this); + } + widget.boardList!.widget.boardView!.draggedItemIndex = null; + widget.boardList!.widget.boardView!.draggedListIndex = null; + if (widget.boardList!.widget.boardView!.listStates[listIndex!].mounted) { + widget.boardList!.widget.boardView!.listStates[listIndex].setState(() {}); + } + } + + void _startDrag(Widget item, BuildContext context) { + if (widget.boardList!.widget.boardView != null) { + widget.boardList!.widget.boardView!.onDropItem = onDropItem; + if (widget.boardList!.mounted) { + widget.boardList!.setState(() {}); + } + widget.boardList!.widget.boardView!.draggedItemIndex = widget.index; + widget.boardList!.widget.boardView!.height = context.size!.height; + widget.boardList!.widget.boardView!.draggedListIndex = + widget.boardList!.widget.index; + widget.boardList!.widget.boardView!.startListIndex = + widget.boardList!.widget.index; + widget.boardList!.widget.boardView!.startItemIndex = widget.index; + widget.boardList!.widget.boardView!.draggedItem = item; + if (widget.onStartDragItem != null) { + widget.onStartDragItem!( + widget.boardList!.widget.index, widget.index, this); + } + widget.boardList!.widget.boardView!.run(); + if (widget.boardList!.widget.boardView!.mounted) { + widget.boardList!.widget.boardView!.setState(() {}); + } + } + } + + void afterFirstLayout(BuildContext context) { + try { + height = context.size!.height; + width = context.size!.width; + } catch (e) {} + } + + @override + Widget build(BuildContext context) { + WidgetsBinding.instance! + .addPostFrameCallback((_) => afterFirstLayout(context)); + if (widget.boardList!.itemStates.length > widget.index!) { + widget.boardList!.itemStates.removeAt(widget.index!); + } + widget.boardList!.itemStates.insert(widget.index!, this); + return GestureDetector( + onTapDown: (otd) { + if (widget.draggable) { + RenderBox object = context.findRenderObject() as RenderBox; + Offset pos = object.localToGlobal(Offset.zero); + RenderBox box = + widget.boardList!.context.findRenderObject() as RenderBox; + Offset listPos = box.localToGlobal(Offset.zero); + widget.boardList!.widget.boardView!.leftListX = listPos.dx; + widget.boardList!.widget.boardView!.topListY = listPos.dy; + widget.boardList!.widget.boardView!.topItemY = pos.dy; + widget.boardList!.widget.boardView!.bottomItemY = + pos.dy + object.size.height; + widget.boardList!.widget.boardView!.bottomListY = + listPos.dy + box.size.height; + widget.boardList!.widget.boardView!.rightListX = + listPos.dx + box.size.width; + + widget.boardList!.widget.boardView!.initialX = pos.dx; + widget.boardList!.widget.boardView!.initialY = pos.dy; + } + }, + onTapCancel: () {}, + onTap: () { + if (widget.onTapItem != null) { + if (widget.boardList != null && + widget.boardList!.widget.index != null && + widget.index != null) { + widget.onTapItem!( + widget.boardList!.widget.index!, widget.index!, this); + } else {} + } + }, + onLongPress: () { + if (!widget.boardList!.widget.boardView!.widget.isSelecting && + widget.draggable) { + _startDrag(widget, context); + } + }, + child: widget.item, + ); + } +} diff --git a/demos/supabase-trello/lib/widgets/thirdparty/board_list.dart b/demos/supabase-trello/lib/widgets/thirdparty/board_list.dart new file mode 100644 index 00000000..5ccdadf3 --- /dev/null +++ b/demos/supabase-trello/lib/widgets/thirdparty/board_list.dart @@ -0,0 +1,188 @@ +import 'board_item.dart'; +import 'boardview.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +typedef void OnDropList(int? listIndex,int? oldListIndex); +typedef void OnTapList(int? listIndex); +typedef void OnStartDragList(int? listIndex); + +class BoardList extends StatefulWidget { + final List? header; + final Widget? footer; + final List? items; + final Color? backgroundColor; + final Color? headerBackgroundColor; + final BoardViewState? boardView; + final OnDropList? onDropList; + final OnTapList? onTapList; + final OnStartDragList? onStartDragList; + final bool draggable; + + const BoardList({ + Key? key, + this.header, + this.items, + this.footer, + this.backgroundColor, + this.headerBackgroundColor, + this.boardView, + this.draggable = true, + this.index, this.onDropList, this.onTapList, this.onStartDragList, + }) : super(key: key); + + final int? index; + + @override + State createState() { + return BoardListState(); + } +} + +class BoardListState extends State with AutomaticKeepAliveClientMixin{ + List itemStates = []; + ScrollController boardListController = new ScrollController(); + + void onDropList(int? listIndex) { + if(widget.onDropList != null){ + widget.onDropList!(listIndex,widget.boardView!.startListIndex); + } + widget.boardView!.draggedListIndex = null; + if(widget.boardView!.mounted) { + widget.boardView!.setState(() { + + }); + } + } + + void _startDrag(Widget item, BuildContext context) { + if (widget.boardView != null && widget.draggable) { + if(widget.onStartDragList != null){ + widget.onStartDragList!(widget.index); + } + widget.boardView!.startListIndex = widget.index; + widget.boardView!.height = context.size!.height; + widget.boardView!.draggedListIndex = widget.index!; + widget.boardView!.draggedItemIndex = null; + widget.boardView!.draggedItem = item; + widget.boardView!.onDropList = onDropList; + widget.boardView!.run(); + if(widget.boardView!.mounted) { + widget.boardView!.setState(() {}); + } + } + } + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + List listWidgets = []; + if (widget.header != null) { + Color? headerBackgroundColor = Color.fromARGB(255, 255, 255, 255); + if (widget.headerBackgroundColor != null) { + headerBackgroundColor = widget.headerBackgroundColor; + } + listWidgets.add(GestureDetector( + onTap: (){ + if(widget.onTapList != null){ + widget.onTapList!(widget.index); + } + }, + onTapDown: (otd) { + if(widget.draggable) { + RenderBox object = context.findRenderObject() as RenderBox; + Offset pos = object.localToGlobal(Offset.zero); + widget.boardView!.initialX = pos.dx; + widget.boardView!.initialY = pos.dy; + + widget.boardView!.rightListX = pos.dx + object.size.width; + widget.boardView!.leftListX = pos.dx; + } + }, + onTapCancel: () {}, + onLongPress: () { + if(!widget.boardView!.widget.isSelecting && widget.draggable) { + _startDrag(widget, context); + } + }, + child: Container( + color: widget.headerBackgroundColor, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: widget.header!), + ))); + + } + if (widget.items != null) { + listWidgets.add(Flexible( + fit: FlexFit.loose, + child: ListView.builder( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + controller: boardListController, + itemCount: widget.items!.length, + itemBuilder: (ctx, index) { + // if index does not exist + if (widget.items!.length <= index) { + //set index to the last index + return Opacity( + opacity: 0.0, + child: widget.boardView!.draggedItem!, + ); + } + + if (widget.items![index].boardList == null || + widget.items![index].index != index || + widget.items![index].boardList!.widget.index != widget.index || + widget.items![index].boardList != this) { + widget.items![index] = BoardItem( + boardList: this, + item: widget.items![index].item, + draggable: widget.items![index].draggable, + index: index, + onDropItem: widget.items![index].onDropItem, + onTapItem: widget.items![index].onTapItem, + onDragItem: widget.items![index].onDragItem, + onStartDragItem: widget.items![index].onStartDragItem, + ); + } + if (widget.boardView!.draggedItemIndex == index && + widget.boardView!.draggedListIndex == widget.index) { + return Opacity( + opacity: 0.0, + child: widget.items![index], + ); + } else { + return widget.items![index]; + } + }, + ))); + } + + if (widget.footer != null) { + listWidgets.add(widget.footer!); + } + + Color? backgroundColor = const Color.fromARGB(255, 255, 255, 255); + + if (widget.backgroundColor != null) { + backgroundColor = widget.backgroundColor; + } + if (widget.boardView!.listStates.length > widget.index!) { + widget.boardView!.listStates.removeAt(widget.index!); + } + widget.boardView!.listStates.insert(widget.index!, this); + + return Container( + margin: const EdgeInsets.all(8), + decoration: BoxDecoration(color: backgroundColor), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: listWidgets as List, + )); + } +} diff --git a/demos/supabase-trello/lib/widgets/thirdparty/boardview.dart b/demos/supabase-trello/lib/widgets/thirdparty/boardview.dart new file mode 100644 index 00000000..d50faf90 --- /dev/null +++ b/demos/supabase-trello/lib/widgets/thirdparty/boardview.dart @@ -0,0 +1,683 @@ +library boardview; + +import 'boardview_controller.dart'; +import 'vs_scrollbar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'dart:core'; +import 'board_list.dart'; + +class BoardView extends StatefulWidget { + final List? lists; + final double width; + Widget? middleWidget; + double? bottomPadding; + bool isSelecting; + bool? scrollbar; + ScrollbarStyle? scrollbarStyle; + BoardViewController? boardViewController; + int dragDelay; + + Function(bool)? itemInMiddleWidget; + OnDropBottomWidget? onDropItemInMiddleWidget; + BoardView({Key? key, this.itemInMiddleWidget,this.scrollbar,this.scrollbarStyle,this.boardViewController,this.dragDelay=300,this.onDropItemInMiddleWidget, this.isSelecting = false, this.lists, this.width = 280, this.middleWidget, this.bottomPadding}) : super(key: key); + + @override + State createState() { + return BoardViewState(); + } +} + +typedef void OnDropBottomWidget(int? listIndex, int? itemIndex,double percentX); +typedef void OnDropItem(int? listIndex, int? itemIndex); +typedef void OnDropList(int? listIndex); + +class BoardViewState extends State with AutomaticKeepAliveClientMixin { + Widget? draggedItem; + int? draggedItemIndex; + int? draggedListIndex; + double? dx; + double? dxInit; + double? dyInit; + double? dy; + double? offsetX; + double? offsetY; + double? initialX = 0; + double? initialY = 0; + double? rightListX; + double? leftListX; + double? topListY; + double? bottomListY; + double? topItemY; + double? bottomItemY; + double? height; + int? startListIndex; + int? startItemIndex; + + bool canDrag = true; + + ScrollController boardViewController = new ScrollController(); + + List listStates = []; + + OnDropItem? onDropItem; + OnDropList? onDropList; + + bool isScrolling = false; + + bool _isInWidget = false; + + GlobalKey _middleWidgetKey = GlobalKey(); + + var pointer; + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + if(widget.boardViewController != null){ + widget.boardViewController!.state = this; + } + } + + void moveDown() { + if(topItemY != null){ + topItemY = topItemY! + listStates[draggedListIndex!].itemStates[draggedItemIndex! + 1].height; + } + if(bottomItemY != null){ + bottomItemY = bottomItemY! + listStates[draggedListIndex!].itemStates[draggedItemIndex! + 1].height; + } + var item = widget.lists![draggedListIndex!].items![draggedItemIndex!]; + widget.lists![draggedListIndex!].items!.removeAt(draggedItemIndex!); + var itemState = listStates[draggedListIndex!].itemStates[draggedItemIndex!]; + listStates[draggedListIndex!].itemStates.removeAt(draggedItemIndex!); + if(draggedItemIndex != null){ + draggedItemIndex = draggedItemIndex! + 1; + } + widget.lists![draggedListIndex!].items!.insert(draggedItemIndex!, item); + listStates[draggedListIndex!].itemStates.insert(draggedItemIndex!, itemState); + if(listStates[draggedListIndex!].mounted) { + listStates[draggedListIndex!].setState(() {}); + } + } + + void moveUp() { + if(topItemY != null){ + topItemY = topItemY! - listStates[draggedListIndex!].itemStates[draggedItemIndex! - 1].height; + } + if(bottomItemY != null){ + bottomItemY = bottomItemY!-listStates[draggedListIndex!].itemStates[draggedItemIndex! - 1].height; + } + var item = widget.lists![draggedListIndex!].items![draggedItemIndex!]; + widget.lists![draggedListIndex!].items!.removeAt(draggedItemIndex!); + var itemState = listStates[draggedListIndex!].itemStates[draggedItemIndex!]; + listStates[draggedListIndex!].itemStates.removeAt(draggedItemIndex!); + if(draggedItemIndex != null){ + draggedItemIndex = draggedItemIndex! - 1; + } + widget.lists![draggedListIndex!].items!.insert(draggedItemIndex!, item); + listStates[draggedListIndex!].itemStates.insert(draggedItemIndex!, itemState); + if(listStates[draggedListIndex!].mounted) { + listStates[draggedListIndex!].setState(() {}); + } + } + + void moveListRight() { + var list = widget.lists![draggedListIndex!]; + var listState = listStates[draggedListIndex!]; + widget.lists!.removeAt(draggedListIndex!); + listStates.removeAt(draggedListIndex!); + if(draggedListIndex != null){ + draggedListIndex = draggedListIndex! + 1; + } + widget.lists!.insert(draggedListIndex!, list); + listStates.insert(draggedListIndex!, listState); + canDrag = false; + if (boardViewController != null && boardViewController.hasClients) { + int? tempListIndex = draggedListIndex; + boardViewController + .animateTo(draggedListIndex! * widget.width, duration: new Duration(milliseconds: 400), curve: Curves.ease) + .whenComplete(() { + RenderBox object = listStates[tempListIndex!].context.findRenderObject() as RenderBox; + Offset pos = object.localToGlobal(Offset.zero); + leftListX = pos.dx; + rightListX = pos.dx + object.size.width; + Future.delayed(new Duration(milliseconds: widget.dragDelay), () { + canDrag = true; + }); + }); + } + if(mounted){ + setState(() {}); + } + } + + void moveRight() { + var item = widget.lists![draggedListIndex!].items![draggedItemIndex!]; + var itemState = listStates[draggedListIndex!].itemStates[draggedItemIndex!]; + widget.lists![draggedListIndex!].items!.removeAt(draggedItemIndex!); + listStates[draggedListIndex!].itemStates.removeAt(draggedItemIndex!); + if(listStates[draggedListIndex!].mounted) { + listStates[draggedListIndex!].setState(() {}); + } + if(draggedListIndex != null){ + draggedListIndex = draggedListIndex! + 1; + } + double closestValue = 10000; + draggedItemIndex = 0; + for (int i = 0; i < listStates[draggedListIndex!].itemStates.length; i++) { + if (listStates[draggedListIndex!].itemStates[i].mounted && listStates[draggedListIndex!].itemStates[i].context != null) { + RenderBox box = listStates[draggedListIndex!].itemStates[i].context.findRenderObject() as RenderBox; + Offset pos = box.localToGlobal(Offset.zero); + var temp = (pos.dy - dy! + (box.size.height / 2)).abs(); + if (temp < closestValue) { + closestValue = temp; + draggedItemIndex = i; + dyInit = dy; + } + } + } + widget.lists![draggedListIndex!].items!.insert(draggedItemIndex!, item); + listStates[draggedListIndex!].itemStates.insert(draggedItemIndex!, itemState); + canDrag = false; + if(listStates[draggedListIndex!].mounted) { + listStates[draggedListIndex!].setState(() {}); + } + if (boardViewController != null && boardViewController.hasClients) { + int? tempListIndex = draggedListIndex; + int? tempItemIndex = draggedItemIndex; + boardViewController + .animateTo(draggedListIndex! * widget.width, duration: new Duration(milliseconds: 400), curve: Curves.ease) + .whenComplete(() { + RenderBox object = listStates[tempListIndex!].context.findRenderObject() as RenderBox; + Offset pos = object.localToGlobal(Offset.zero); + leftListX = pos.dx; + rightListX = pos.dx + object.size.width; + RenderBox box = listStates[tempListIndex].itemStates[tempItemIndex!].context.findRenderObject() as RenderBox; + Offset itemPos = box.localToGlobal(Offset.zero); + topItemY = itemPos.dy; + bottomItemY = itemPos.dy + box.size.height; + Future.delayed(new Duration(milliseconds: widget.dragDelay), () { + canDrag = true; + }); + }); + } + if(mounted){ + setState(() { }); + } + } + + void moveListLeft() { + var list = widget.lists![draggedListIndex!]; + var listState = listStates[draggedListIndex!]; + widget.lists!.removeAt(draggedListIndex!); + listStates.removeAt(draggedListIndex!); + if(draggedListIndex != null){ + draggedListIndex = draggedListIndex! - 1; + } + widget.lists!.insert(draggedListIndex!, list); + listStates.insert(draggedListIndex!, listState); + canDrag = false; + if (boardViewController != null && boardViewController.hasClients) { + int? tempListIndex = draggedListIndex; + boardViewController + .animateTo(draggedListIndex! * widget.width, duration: new Duration(milliseconds: widget.dragDelay), curve: Curves.ease) + .whenComplete(() { + RenderBox object = listStates[tempListIndex!].context.findRenderObject() as RenderBox; + Offset pos = object.localToGlobal(Offset.zero); + leftListX = pos.dx; + rightListX = pos.dx + object.size.width; + Future.delayed(new Duration(milliseconds: widget.dragDelay), () { + canDrag = true; + }); + }); + } + if(mounted) { + setState(() {}); + } + } + + void moveLeft() { + var item = widget.lists![draggedListIndex!].items![draggedItemIndex!]; + var itemState = listStates[draggedListIndex!].itemStates[draggedItemIndex!]; + widget.lists![draggedListIndex!].items!.removeAt(draggedItemIndex!); + listStates[draggedListIndex!].itemStates.removeAt(draggedItemIndex!); + if(listStates[draggedListIndex!].mounted) { + listStates[draggedListIndex!].setState(() {}); + } + if(draggedListIndex != null){ + draggedListIndex = draggedListIndex! - 1; + } + double closestValue = 10000; + draggedItemIndex = 0; + for (int i = 0; i < listStates[draggedListIndex!].itemStates.length; i++) { + if (listStates[draggedListIndex!].itemStates[i].mounted && listStates[draggedListIndex!].itemStates[i].context != null) { + RenderBox box = listStates[draggedListIndex!].itemStates[i].context.findRenderObject() as RenderBox; + Offset pos = box.localToGlobal(Offset.zero); + var temp = (pos.dy - dy! + (box.size.height / 2)).abs(); + if (temp < closestValue) { + closestValue = temp; + draggedItemIndex = i; + dyInit = dy; + } + } + } + widget.lists![draggedListIndex!].items!.insert(draggedItemIndex!, item); + listStates[draggedListIndex!].itemStates.insert(draggedItemIndex!, itemState); + canDrag = false; + if(listStates[draggedListIndex!].mounted) { + listStates[draggedListIndex!].setState(() {}); + } + if (boardViewController != null && boardViewController.hasClients) { + int? tempListIndex = draggedListIndex; + int? tempItemIndex = draggedItemIndex; + boardViewController + .animateTo(draggedListIndex! * widget.width, duration: new Duration(milliseconds: 400), curve: Curves.ease) + .whenComplete(() { + RenderBox object = listStates[tempListIndex!].context.findRenderObject() as RenderBox; + Offset pos = object.localToGlobal(Offset.zero); + leftListX = pos.dx; + rightListX = pos.dx + object.size.width; + RenderBox box = listStates[tempListIndex].itemStates[tempItemIndex!].context.findRenderObject() as RenderBox; + Offset itemPos = box.localToGlobal(Offset.zero); + topItemY = itemPos.dy; + bottomItemY = itemPos.dy + box.size.height; + Future.delayed(new Duration(milliseconds: widget.dragDelay), () { + canDrag = true; + }); + }); + } + if(mounted) { + setState(() {}); + } + } + + bool shown = true; + + @override + Widget build(BuildContext context) { + // print("dy:${dy}"); + // print("topListY:${topListY}"); + // print("bottomListY:${bottomListY}"); + if(boardViewController.hasClients) { + WidgetsBinding.instance!.addPostFrameCallback((Duration duration) { + try { + boardViewController.position.didUpdateScrollPositionBy(0); + }catch(e){} + bool _shown = boardViewController.position.maxScrollExtent!=0; + if(_shown != shown){ + setState(() { + shown = _shown; + }); + } + }); + } + Widget listWidget = ListView.builder( + physics: ClampingScrollPhysics(), + itemCount: widget.lists!.length, + scrollDirection: Axis.horizontal, + controller: boardViewController, + itemBuilder: (BuildContext context, int index) { + // if index does not exist on lists + if (widget.lists!.length <= index) { + print('Returned empty container'); + return Container(); + } + + if (widget.lists![index].boardView == null) { + widget.lists![index] = BoardList( + items: widget.lists![index].items, + headerBackgroundColor: widget.lists![index].headerBackgroundColor, + backgroundColor: widget.lists![index].backgroundColor, + footer: widget.lists![index].footer, + header: widget.lists![index].header, + boardView: this, + draggable: widget.lists![index].draggable, + onDropList: widget.lists![index].onDropList, + onTapList: widget.lists![index].onTapList, + onStartDragList: widget.lists![index].onStartDragList, + ); + } + if (widget.lists![index].index != index) { + widget.lists![index] = BoardList( + items: widget.lists![index].items, + headerBackgroundColor: widget.lists![index].headerBackgroundColor, + backgroundColor: widget.lists![index].backgroundColor, + footer: widget.lists![index].footer, + header: widget.lists![index].header, + boardView: this, + draggable: widget.lists![index].draggable, + index: index, + onDropList: widget.lists![index].onDropList, + onTapList: widget.lists![index].onTapList, + onStartDragList: widget.lists![index].onStartDragList, + ); + } + + var temp = Container( + width: widget.width, + padding: EdgeInsets.fromLTRB(0, 0, 0, widget.bottomPadding ?? 0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [Expanded(child: widget.lists![index])], + )); + if (draggedListIndex == index && draggedItemIndex == null) { + return Opacity( + opacity: 0.0, + child: temp, + ); + } else { + return temp; + } + }, + ); + if(widget.scrollbar == true){ + listWidget = VsScrollbar( + controller: boardViewController, + showTrackOnHover: true,// default false + isAlwaysShown: shown&&widget.lists!.length>1, // default false + scrollbarFadeDuration: Duration(milliseconds: 500), // default : Duration(milliseconds: 300) + scrollbarTimeToFade: Duration(milliseconds: 800),// default : Duration(milliseconds: 600) + style: widget.scrollbarStyle!=null?VsScrollbarStyle( + hoverThickness: widget.scrollbarStyle!.hoverThickness, + radius: widget.scrollbarStyle!.radius, + thickness: widget.scrollbarStyle!.thickness, + color: widget.scrollbarStyle!.color + ):VsScrollbarStyle(), + child:listWidget); + } + List stackWidgets = [ + listWidget + ]; + bool isInBottomWidget = false; + if (dy != null) { + if (MediaQuery.of(context).size.height - dy! < 80) { + isInBottomWidget = true; + } + } + if(widget.itemInMiddleWidget != null && _isInWidget != isInBottomWidget) { + widget.itemInMiddleWidget!(isInBottomWidget); + _isInWidget = isInBottomWidget; + } + if (initialX != null && + initialY != null && + offsetX != null && + offsetY != null && + dx != null && + dy != null && + height != null && + widget.width != null) { + if (canDrag && dxInit != null && dyInit != null && !isInBottomWidget) { + if (draggedItemIndex != null && draggedItem != null && topItemY != null && bottomItemY != null) { + //dragging item + if (0 <= draggedListIndex! - 1 && dx! < leftListX! + 45) { + //scroll left + if (boardViewController != null && boardViewController.hasClients) { + boardViewController.animateTo(boardViewController.position.pixels - 5, + duration: new Duration(milliseconds: 10), curve: Curves.ease); + if(listStates[draggedListIndex!].mounted) { + RenderBox object = listStates[draggedListIndex!].context + .findRenderObject() as RenderBox; + Offset pos = object.localToGlobal(Offset.zero); + leftListX = pos.dx; + rightListX = pos.dx + object.size.width; + } + } + } + if (widget.lists!.length > draggedListIndex! + 1 && dx! > rightListX! - 45) { + //scroll right + if (boardViewController != null && boardViewController.hasClients) { + boardViewController.animateTo(boardViewController.position.pixels + 5, + duration: new Duration(milliseconds: 10), curve: Curves.ease); + if(listStates[draggedListIndex!].mounted) { + RenderBox object = listStates[draggedListIndex!].context + .findRenderObject() as RenderBox; + Offset pos = object.localToGlobal(Offset.zero); + leftListX = pos.dx; + rightListX = pos.dx + object.size.width; + } + } + } + if (0 <= draggedListIndex! - 1 && dx! < leftListX!) { + //move left + moveLeft(); + } + if (widget.lists!.length > draggedListIndex! + 1 && dx! > rightListX!) { + //move right + moveRight(); + } + if (dy! < topListY! + 70) { + //scroll up + if (listStates[draggedListIndex!].boardListController != null && + listStates[draggedListIndex!].boardListController.hasClients && !isScrolling) { + isScrolling = true; + double pos = listStates[draggedListIndex!].boardListController.position.pixels; + listStates[draggedListIndex!].boardListController.animateTo( + listStates[draggedListIndex!].boardListController.position.pixels - 5, + duration: new Duration(milliseconds: 10), + curve: Curves.ease).whenComplete((){ + + pos -= listStates[draggedListIndex!].boardListController.position.pixels; + if(initialY == null) + initialY = 0; +// if(widget.boardViewController != null) { +// initialY -= pos; +// } + isScrolling = false; + if(topItemY != null) { + topItemY = topItemY! + pos; + } + if(bottomItemY != null) { + bottomItemY = bottomItemY! + pos; + } + if(mounted){ + setState(() { }); + } + }); + } + } + if (0 <= draggedItemIndex! - 1 && + dy! < topItemY! - listStates[draggedListIndex!].itemStates[draggedItemIndex! - 1].height / 2) { + //move up + moveUp(); + } + double? tempBottom = bottomListY; + if(widget.middleWidget != null){ + if(_middleWidgetKey.currentContext != null) { + RenderBox _box = _middleWidgetKey.currentContext! + .findRenderObject() as RenderBox; + tempBottom = _box.size.height; + print("tempBottom:${tempBottom}"); + } + } + if (dy! > tempBottom! - 70) { + //scroll down + + if (listStates.length < draggedListIndex! && listStates[draggedListIndex!].boardListController != null && + listStates[draggedListIndex!].boardListController.hasClients) { + isScrolling = true; + double pos = listStates[draggedListIndex!].boardListController.position.pixels; + listStates[draggedListIndex!].boardListController.animateTo( + listStates[draggedListIndex!].boardListController.position.pixels + 5, + duration: new Duration(milliseconds: 10), + curve: Curves.ease).whenComplete((){ + pos -= listStates[draggedListIndex!].boardListController.position.pixels; + initialY ??= 0; +// if(widget.boardViewController != null) { +// initialY -= pos; +// } + isScrolling = false; + if(topItemY != null) { + topItemY = topItemY! + pos; + } + if(bottomItemY != null) { + bottomItemY = bottomItemY! + pos; + } + if(mounted){ + setState(() {}); + } + }); + } + } + if (widget.lists![draggedListIndex!].items!.length > draggedItemIndex! + 1 && + dy! > bottomItemY! + listStates[draggedListIndex!].itemStates[draggedItemIndex! + 1].height / 2) { + //move down + moveDown(); + } + } else { + //dragging list + if (0 <= draggedListIndex! - 1 && dx! < leftListX! + 45) { + //scroll left + if (boardViewController != null && boardViewController.hasClients) { + boardViewController.animateTo(boardViewController.position.pixels - 5, + duration: new Duration(milliseconds: 10), curve: Curves.ease); + if(leftListX != null){ + leftListX = leftListX! + 5; + } + if(rightListX != null){ + rightListX = rightListX! + 5; + } + } + } + + if (widget.lists!.length > draggedListIndex! + 1 && dx! > rightListX! - 45) { + //scroll right + if (boardViewController != null && boardViewController.hasClients) { + boardViewController.animateTo(boardViewController.position.pixels + 5, + duration: new Duration(milliseconds: 10), curve: Curves.ease); + if(leftListX != null){ + leftListX = leftListX! - 5; + } + if(rightListX != null){ + rightListX = rightListX! - 5; + } + } + } + if (widget.lists!.length > draggedListIndex! + 1 && dx! > rightListX!) { + //move right + moveListRight(); + } + if (0 <= draggedListIndex! - 1 && dx! < leftListX!) { + //move left + moveListLeft(); + } + } + } + if (widget.middleWidget != null) { + stackWidgets.add(Container(key:_middleWidgetKey,child:widget.middleWidget)); + } + WidgetsBinding.instance!.addPostFrameCallback((timeStamp) { + if(mounted){ + setState(() {}); + } + }); + stackWidgets.add(Positioned( + width: widget.width, + height: height, + child: Opacity(opacity: .7, child: draggedItem), + left: (dx! - offsetX!) + initialX!, + top: (dy! - offsetY!) + initialY!, + )); + } + + return Container( + child: Listener( + onPointerMove: (opm) { + if (draggedItem != null) { + if (dxInit == null) { + dxInit = opm.position.dx; + } + if (dyInit == null) { + dyInit = opm.position.dy; + } + dx = opm.position.dx; + dy = opm.position.dy; + if(mounted) { + setState(() {}); + } + } + }, + onPointerDown: (opd) { + RenderBox box = context.findRenderObject() as RenderBox; + Offset pos = box.localToGlobal(opd.position); + offsetX = pos.dx; + offsetY = pos.dy; + pointer = opd; + if(mounted) { + setState(() {}); + } + }, + onPointerUp: (opu) { + if (onDropItem != null) { + int? tempDraggedItemIndex = draggedItemIndex; + int? tempDraggedListIndex = draggedListIndex; + int? startDraggedItemIndex = startItemIndex; + int? startDraggedListIndex = startListIndex; + + if(_isInWidget && widget.onDropItemInMiddleWidget != null){ + onDropItem!(startDraggedListIndex, startDraggedItemIndex); + widget.onDropItemInMiddleWidget!(startDraggedListIndex, startDraggedItemIndex,opu.position.dx/MediaQuery.of(context).size.width); + }else{ + onDropItem!(tempDraggedListIndex, tempDraggedItemIndex); + } + } + if (onDropList != null) { + int? tempDraggedListIndex = draggedListIndex; + if(_isInWidget && widget.onDropItemInMiddleWidget != null){ + onDropList!(tempDraggedListIndex); + widget.onDropItemInMiddleWidget!(tempDraggedListIndex,null,opu.position.dx/MediaQuery.of(context).size.width); + }else{ + onDropList!(tempDraggedListIndex); + } + } + draggedItem = null; + offsetX = null; + offsetY = null; + initialX = null; + initialY = null; + dx = null; + dy = null; + draggedItemIndex = null; + draggedListIndex = null; + onDropItem = null; + onDropList = null; + dxInit = null; + dyInit = null; + leftListX = null; + rightListX = null; + topListY = null; + bottomListY = null; + topItemY = null; + bottomItemY = null; + startListIndex = null; + startItemIndex = null; + if(mounted) { + setState(() {}); + } + }, + child: new Stack( + children: stackWidgets, + ))); + } + + void run() { + if (pointer != null) { + dx = pointer.position.dx; + dy = pointer.position.dy; + if(mounted) { + setState(() {}); + } + } + } +} + +class ScrollbarStyle{ + double hoverThickness; + double thickness; + Radius radius; + Color color; + ScrollbarStyle({this.radius = const Radius.circular(10),this.hoverThickness = 10,this.thickness = 10,this.color = Colors.black}); +} diff --git a/demos/supabase-trello/lib/widgets/thirdparty/boardview_controller.dart b/demos/supabase-trello/lib/widgets/thirdparty/boardview_controller.dart new file mode 100644 index 00000000..74c5af04 --- /dev/null +++ b/demos/supabase-trello/lib/widgets/thirdparty/boardview_controller.dart @@ -0,0 +1,18 @@ +import 'package:flutter/animation.dart'; + +import 'boardview.dart'; + +class BoardViewController{ + + BoardViewController(); + + late BoardViewState state; + + Future animateTo(int index,{Duration? duration,Curve? curve})async{ + double offset = index * state.widget.width; + if (state.boardViewController != null && state.boardViewController.hasClients) { + await state.boardViewController.animateTo( + offset, duration: duration!, curve: curve!); + } + } +} \ No newline at end of file diff --git a/demos/supabase-trello/lib/widgets/thirdparty/vs_scrollbar.dart b/demos/supabase-trello/lib/widgets/thirdparty/vs_scrollbar.dart new file mode 100644 index 00000000..85aa58bb --- /dev/null +++ b/demos/supabase-trello/lib/widgets/thirdparty/vs_scrollbar.dart @@ -0,0 +1,348 @@ +library vs_scrollbar; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +class VsScrollbarStyle { + /// The hoverThickness of the VsScrollbar thumb. + /// default value is 12.0 pixels. + final double hoverThickness; + + /// The thickness of the VsScrollbar thumb. + /// default thickness of 8.0 pixels. + final double thickness; + + /// The radius of the VsScrollbar thumb. + /// default [Radius.circular] of 8.0 pixels. + final Radius radius; + + /// The color of the VsScrollbar thumb. + final Color? color; + + const VsScrollbarStyle( + {this.radius = _kScrollbarRadius, + this.thickness = _kScrollbarThickness, + this.hoverThickness = _kScrollbarThicknessWithTrack, + this.color}); +} + +const VsScrollbarStyle _kScrollbarStyle = const VsScrollbarStyle(); +const double _kScrollbarThickness = 8.0; +const double _kScrollbarThicknessWithTrack = 12.0; +const double _kScrollbarMargin = 2.0; +const double _kScrollbarMinLength = 48.0; +const Radius _kScrollbarRadius = Radius.circular(8.0); +const Duration _kScrollbarFadeDuration = Duration(milliseconds: 300); +const Duration _kScrollbarTimeToFade = Duration(milliseconds: 600); + +/// To add a VsScrollbar to a [ScrollView], wrap the scroll view +/// widget in a [VsScrollbar] widget. +/// +/// The color of the VsScrollbar will change when dragged. A hover animation is +/// also triggered when used on web and desktop platforms. A VsScrollbar track +/// can also been drawn when triggered by a hover event, which is controlled by +/// [showTrackOnHover]. The thickness of the track and VsScrollbar thumb will +/// become larger when hovering, unless overridden by [hoverThickness]. +/// +/// See also: +/// +/// * [RawScrollbar], a basic VsScrollbar that fades in and out, extended +/// by this class to add more animations and behaviors. +/// * [ScrollbarTheme], which configures the VsScrollbar's appearance. +/// * [ListView], which displays a linear, scrollable list of children. +/// * [GridView], which displays a 2 dimensional, scrollable array of children. +class VsScrollbar extends StatefulWidget { + /// Creates a material design VsScrollbar that by default will connect to the + /// closest Scrollable descendant of [child]. + /// + /// The [child] should be a source of [ScrollNotification] notifications, + /// typically a [Scrollable] widget. + /// + /// If the [controller] is null, the default behavior is to + /// enable VsScrollbar dragging using the [PrimaryScrollController]. + /// + const VsScrollbar({ + Key? key, + required this.child, + this.controller, + this.style = _kScrollbarStyle, + this.scrollbarFadeDuration, + this.scrollbarTimeToFade, + this.isAlwaysShown, + this.showTrackOnHover, + this.notificationPredicate, + }) : super(key: key); + + /// {@macro flutter.widgets.VsScrollbar.child} + final Widget child; + + /// {@macro flutter.widgets.VsScrollbar.controller} + final ScrollController? controller; + + /// {@macro flutter.widgets.VsScrollbar.isAlwaysShown} + final bool? isAlwaysShown; + + /// If this property is null, then [ScrollbarThemeData.showTrackOnHover] of + /// [ThemeData.scrollbarTheme] is used. If that is also null, the default value + /// is false. + final bool? showTrackOnHover; + + ///Style Property for VsScrollbar + final VsScrollbarStyle style; + + /// {@macro flutter.widgets.VsScrollbar.notificationPredicate} + final ScrollNotificationPredicate? notificationPredicate; + + /// default 600ms + final Duration? scrollbarTimeToFade; + + /// default 300ms + final Duration? scrollbarFadeDuration; + + @override + _ScrollbarState createState() => _ScrollbarState(); +} + +class _ScrollbarState extends State { + @override + Widget build(BuildContext context) { + return _MaterialScrollbar( + child: widget.child, + controller: widget.controller, + isAlwaysShown: widget.isAlwaysShown, + showTrackOnHover: widget.showTrackOnHover, + hoverThickness: widget.style.hoverThickness, + thickness: widget.style.thickness, + radius: widget.style.radius, + color: widget.style.color, + notificationPredicate: widget.notificationPredicate, + ); + } +} + +class _MaterialScrollbar extends RawScrollbar { + const _MaterialScrollbar({ + Key? key, + required Widget child, + ScrollController? controller, + bool? isAlwaysShown, + this.showTrackOnHover, + this.hoverThickness, + this.color, + this.scrollbarFadeDuration, + this.scrollbarTimeToFade, + double? thickness, + Radius? radius, + ScrollNotificationPredicate? notificationPredicate, + }) : super( + key: key, + child: child, + controller: controller, + thumbVisibility: isAlwaysShown, + thickness: thickness, + radius: radius, + fadeDuration: scrollbarFadeDuration ?? _kScrollbarFadeDuration, + timeToFade: scrollbarTimeToFade ?? _kScrollbarTimeToFade, + pressDuration: Duration.zero, + notificationPredicate: + notificationPredicate ?? defaultScrollNotificationPredicate, + ); + final Duration? scrollbarTimeToFade; + final Duration? scrollbarFadeDuration; + + final Color? color; + final bool? showTrackOnHover; + final double? hoverThickness; + + @override + _MaterialScrollbarState createState() => _MaterialScrollbarState(); +} + +class _MaterialScrollbarState extends RawScrollbarState<_MaterialScrollbar> { + late AnimationController _hoverAnimationController; + bool _dragIsActive = true; + bool _hoverIsActive = false; + late ColorScheme _colorScheme; + late ScrollbarThemeData _scrollbarTheme; + // On Android, scrollbars should match native appearance. + late bool _useAndroidScrollbar; + + @override + bool get showScrollbar => + widget.thumbVisibility ?? + _scrollbarTheme.thumbVisibility + ?.resolve(Set.of([MaterialState.disabled])) ?? + false; + + bool get _showTrackOnHover => widget.showTrackOnHover ?? false; + + Set get _states => { + if (_dragIsActive) MaterialState.dragged, + if (_hoverIsActive) MaterialState.hovered, + }; + + MaterialStateProperty get _thumbColor { + final Color onSurface = widget.color ?? _colorScheme.onSurface; + late Color dragColor; + late Color hoverColor; + late Color idleColor; + dragColor = onSurface.withOpacity(0.9); + hoverColor = onSurface.withOpacity(0.75); + idleColor = onSurface.withOpacity(0.5); + + return MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.dragged)) + return _scrollbarTheme.thumbColor?.resolve(states) ?? dragColor; + + // If the track is visible, the thumb color hover animation is ignored and + // changes immediately. + if (states.contains(MaterialState.hovered) && _showTrackOnHover) + return _scrollbarTheme.thumbColor?.resolve(states) ?? hoverColor; + + return Color.lerp( + _scrollbarTheme.thumbColor?.resolve(states) ?? idleColor, + _scrollbarTheme.thumbColor?.resolve(states) ?? hoverColor, + _hoverAnimationController.value, + )!; + }); + } + + MaterialStateProperty get _trackColor { + final Color onSurface = widget.color ?? _colorScheme.onSurface; + + return MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.hovered) && _showTrackOnHover) { + return _scrollbarTheme.trackColor?.resolve(states) ?? + onSurface.withOpacity(0.05); + } + return const Color(0x00000000); + }); + } + + MaterialStateProperty get _trackBorderColor { + final Color onSurface = widget.color ?? _colorScheme.onSurface; + + return MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.hovered) && _showTrackOnHover) { + return _scrollbarTheme.trackBorderColor?.resolve(states) ?? + onSurface.withOpacity(0.1); + } + return const Color(0x00000000); + }); + } + + MaterialStateProperty get _thickness { + return MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.hovered) && _showTrackOnHover) + return widget.hoverThickness ?? + _scrollbarTheme.thickness?.resolve(states) ?? + _kScrollbarThicknessWithTrack; + // The default VsScrollbar thickness is smaller on mobile. + return widget.thickness ?? + _scrollbarTheme.thickness?.resolve(states) ?? + (_kScrollbarThickness / (_useAndroidScrollbar ? 2 : 1)); + }); + } + + @override + void initState() { + super.initState(); + _hoverAnimationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 200), + ); + _hoverAnimationController.addListener(() { + updateScrollbarPainter(); + }); + } + + @override + void didChangeDependencies() { + final ThemeData theme = Theme.of(context); + _colorScheme = theme.colorScheme; + _scrollbarTheme = theme.scrollbarTheme; + switch (theme.platform) { + case TargetPlatform.android: + _useAndroidScrollbar = true; + break; + case TargetPlatform.iOS: + case TargetPlatform.linux: + case TargetPlatform.fuchsia: + case TargetPlatform.macOS: + case TargetPlatform.windows: + _useAndroidScrollbar = false; + break; + } + super.didChangeDependencies(); + } + + @override + void updateScrollbarPainter() { + scrollbarPainter + ..color = _thumbColor.resolve(_states) + ..trackColor = _trackColor.resolve(_states) + ..trackBorderColor = _trackBorderColor.resolve(_states) + ..textDirection = Directionality.of(context) + ..thickness = _thickness.resolve(_states) + ..radius = widget.radius ?? + _scrollbarTheme.radius ?? + (_useAndroidScrollbar ? null : _kScrollbarRadius) + ..crossAxisMargin = _scrollbarTheme.crossAxisMargin ?? + (_useAndroidScrollbar ? 0.0 : _kScrollbarMargin) + ..mainAxisMargin = _scrollbarTheme.mainAxisMargin ?? 0.0 + ..minLength = _scrollbarTheme.minThumbLength ?? _kScrollbarMinLength + ..padding = EdgeInsets.all(0); + } + + @override + void handleThumbPressStart(Offset localPosition) { + super.handleThumbPressStart(localPosition); + setState(() { + _dragIsActive = true; + }); + } + + @override + void handleThumbPressEnd(Offset localPosition, Velocity velocity) { + super.handleThumbPressEnd(localPosition, velocity); + setState(() { + _dragIsActive = false; + }); + } + + @override + void handleHover(PointerHoverEvent event) { + super.handleHover(event); + // Check if the position of the pointer falls over the painted VsScrollbar + if (isPointerOverScrollbar(event.position, PointerDeviceKind.mouse)) { + // Pointer is hovering over the VsScrollbar + setState(() { + _hoverIsActive = true; + }); + _hoverAnimationController.forward(); + } else if (_hoverIsActive) { + // Pointer was, but is no longer over painted VsScrollbar. + setState(() { + _hoverIsActive = false; + }); + _hoverAnimationController.reverse(); + } + } + + @override + void handleHoverExit(PointerExitEvent event) { + super.handleHoverExit(event); + setState(() { + _hoverIsActive = false; + }); + _hoverAnimationController.reverse(); + } + + @override + void dispose() { + _hoverAnimationController.dispose(); + super.dispose(); + } +} diff --git a/demos/supabase-trello/linux/.gitignore b/demos/supabase-trello/linux/.gitignore new file mode 100644 index 00000000..d3896c98 --- /dev/null +++ b/demos/supabase-trello/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/demos/supabase-trello/linux/CMakeLists.txt b/demos/supabase-trello/linux/CMakeLists.txt new file mode 100644 index 00000000..edd5d8f3 --- /dev/null +++ b/demos/supabase-trello/linux/CMakeLists.txt @@ -0,0 +1,139 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "trelloappclone_flutter") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.trelloappclone_flutter") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/demos/supabase-trello/linux/flutter/CMakeLists.txt b/demos/supabase-trello/linux/flutter/CMakeLists.txt new file mode 100644 index 00000000..d5bd0164 --- /dev/null +++ b/demos/supabase-trello/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/demos/supabase-trello/linux/flutter/generated_plugin_registrant.cc b/demos/supabase-trello/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..129b6b2f --- /dev/null +++ b/demos/supabase-trello/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,31 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); + g_autoptr(FlPluginRegistrar) powersync_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "PowersyncFlutterLibsPlugin"); + powersync_flutter_libs_plugin_register_with_registrar(powersync_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); + sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/demos/supabase-trello/linux/flutter/generated_plugin_registrant.h b/demos/supabase-trello/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..e0f0a47b --- /dev/null +++ b/demos/supabase-trello/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/demos/supabase-trello/linux/flutter/generated_plugins.cmake b/demos/supabase-trello/linux/flutter/generated_plugins.cmake new file mode 100644 index 00000000..c9e9c841 --- /dev/null +++ b/demos/supabase-trello/linux/flutter/generated_plugins.cmake @@ -0,0 +1,28 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux + gtk + powersync_flutter_libs + sqlite3_flutter_libs + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/demos/supabase-trello/linux/main.cc b/demos/supabase-trello/linux/main.cc new file mode 100644 index 00000000..e7c5c543 --- /dev/null +++ b/demos/supabase-trello/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/demos/supabase-trello/linux/my_application.cc b/demos/supabase-trello/linux/my_application.cc new file mode 100644 index 00000000..e91e0138 --- /dev/null +++ b/demos/supabase-trello/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "trelloappclone_flutter"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "trelloappclone_flutter"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/demos/supabase-trello/linux/my_application.h b/demos/supabase-trello/linux/my_application.h new file mode 100644 index 00000000..72271d5e --- /dev/null +++ b/demos/supabase-trello/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/demos/supabase-trello/macos/.gitignore b/demos/supabase-trello/macos/.gitignore new file mode 100644 index 00000000..746adbb6 --- /dev/null +++ b/demos/supabase-trello/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/demos/supabase-trello/macos/Flutter/Flutter-Debug.xcconfig b/demos/supabase-trello/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 00000000..4b81f9b2 --- /dev/null +++ b/demos/supabase-trello/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/demos/supabase-trello/macos/Flutter/Flutter-Release.xcconfig b/demos/supabase-trello/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 00000000..5caa9d15 --- /dev/null +++ b/demos/supabase-trello/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/demos/supabase-trello/macos/Flutter/GeneratedPluginRegistrant.swift b/demos/supabase-trello/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 00000000..6bf5ce92 --- /dev/null +++ b/demos/supabase-trello/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,24 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import app_links +import file_selector_macos +import path_provider_foundation +import powersync_flutter_libs +import shared_preferences_foundation +import sqlite3_flutter_libs +import url_launcher_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + PowersyncFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "PowersyncFlutterLibsPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) +} diff --git a/demos/supabase-trello/macos/Podfile b/demos/supabase-trello/macos/Podfile new file mode 100644 index 00000000..c795730d --- /dev/null +++ b/demos/supabase-trello/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-trello/macos/Podfile.lock b/demos/supabase-trello/macos/Podfile.lock new file mode 100644 index 00000000..db99ed48 --- /dev/null +++ b/demos/supabase-trello/macos/Podfile.lock @@ -0,0 +1,78 @@ +PODS: + - app_links (1.0.0): + - FlutterMacOS + - file_selector_macos (0.0.1): + - FlutterMacOS + - FlutterMacOS (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sign_in_with_apple (0.0.1): + - FlutterMacOS + - sqlite3 (3.44.0): + - sqlite3/common (= 3.44.0) + - sqlite3/common (3.44.0) + - sqlite3/fts5 (3.44.0): + - sqlite3/common + - sqlite3/perf-threadsafe (3.44.0): + - sqlite3/common + - sqlite3/rtree (3.44.0): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - FlutterMacOS + - sqlite3 (~> 3.44.0) + - sqlite3/fts5 + - sqlite3/perf-threadsafe + - sqlite3/rtree + - url_launcher_macos (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) + - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - sign_in_with_apple (from `Flutter/ephemeral/.symlinks/plugins/sign_in_with_apple/macos`) + - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + +SPEC REPOS: + trunk: + - sqlite3 + +EXTERNAL SOURCES: + app_links: + :path: Flutter/ephemeral/.symlinks/plugins/app_links/macos + file_selector_macos: + :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos + FlutterMacOS: + :path: Flutter/ephemeral + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + sign_in_with_apple: + :path: Flutter/ephemeral/.symlinks/plugins/sign_in_with_apple/macos + sqlite3_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + +SPEC CHECKSUMS: + app_links: 4481ed4d71f384b0c3ae5016f4633aa73d32ff67 + file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 + sign_in_with_apple: a9e97e744e8edc36aefc2723111f652102a7a727 + sqlite3: 6e2d4a4879854d0ec86b476bf3c3e30870bac273 + sqlite3_flutter_libs: a25f3a0f522fdcd8fef6a4a50a3d681dd43d8dea + url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 + +PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 + +COCOAPODS: 1.13.0 diff --git a/demos/supabase-trello/macos/Runner.xcodeproj/project.pbxproj b/demos/supabase-trello/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..410d47d9 --- /dev/null +++ b/demos/supabase-trello/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,791 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 4E4963C86BC785F23F648E0B /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A80D2BD607CA38846657D9B0 /* Pods_RunnerTests.framework */; }; + 9E95FABB7D4E5912BF4FE47B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D41298BB1DE04D5251142AE7 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 2D2712CAF1EB5DE8C09B8534 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* trelloappclone_flutter.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = trelloappclone_flutter.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 4D2CF99A3C595901E898A5A1 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 5E01E450DF23BB04BA05D6DF /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 7D86C2AB85DE03E2BC92BD82 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + 9F1765F95B32AB229EDC81A4 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + A80D2BD607CA38846657D9B0 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D1E469A34757CC55006C694C /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + D41298BB1DE04D5251142AE7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4E4963C86BC785F23F648E0B /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9E95FABB7D4E5912BF4FE47B /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 28F54DBEE7BF5990E862A72B /* Pods */ = { + isa = PBXGroup; + children = ( + 9F1765F95B32AB229EDC81A4 /* Pods-Runner.debug.xcconfig */, + 7D86C2AB85DE03E2BC92BD82 /* Pods-Runner.release.xcconfig */, + 5E01E450DF23BB04BA05D6DF /* Pods-Runner.profile.xcconfig */, + 2D2712CAF1EB5DE8C09B8534 /* Pods-RunnerTests.debug.xcconfig */, + 4D2CF99A3C595901E898A5A1 /* Pods-RunnerTests.release.xcconfig */, + D1E469A34757CC55006C694C /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 28F54DBEE7BF5990E862A72B /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* trelloappclone_flutter.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D41298BB1DE04D5251142AE7 /* Pods_Runner.framework */, + A80D2BD607CA38846657D9B0 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 125EE0D4B5332F47B3E8DC59 /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 95B0EDBAFB4503A28905DD2B /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + DFB0D194C13543172E694703 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* trelloappclone_flutter.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1430; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 125EE0D4B5332F47B3E8DC59 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 95B0EDBAFB4503A28905DD2B /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + DFB0D194C13543172E694703 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2D2712CAF1EB5DE8C09B8534 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.trelloappcloneFlutter.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/trelloappclone_flutter.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/trelloappclone_flutter"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4D2CF99A3C595901E898A5A1 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.trelloappcloneFlutter.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/trelloappclone_flutter.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/trelloappclone_flutter"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D1E469A34757CC55006C694C /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.trelloappcloneFlutter.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/trelloappclone_flutter.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/trelloappclone_flutter"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/demos/supabase-trello/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demos/supabase-trello/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/demos/supabase-trello/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/demos/supabase-trello/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demos/supabase-trello/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..9a756df2 --- /dev/null +++ b/demos/supabase-trello/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/supabase-trello/macos/Runner.xcworkspace/contents.xcworkspacedata b/demos/supabase-trello/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/demos/supabase-trello/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/demos/supabase-trello/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demos/supabase-trello/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/demos/supabase-trello/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/demos/supabase-trello/macos/Runner/AppDelegate.swift b/demos/supabase-trello/macos/Runner/AppDelegate.swift new file mode 100644 index 00000000..d53ef643 --- /dev/null +++ b/demos/supabase-trello/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..a2ec33f1 --- /dev/null +++ b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 00000000..82b6f9d9 Binary files /dev/null and b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 00000000..13b35eba Binary files /dev/null and b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 00000000..0a3f5fa4 Binary files /dev/null and b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 00000000..bdb57226 Binary files /dev/null and b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 00000000..f083318e Binary files /dev/null and b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 00000000..326c0e72 Binary files /dev/null and b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 00000000..2f1632cf Binary files /dev/null and b/demos/supabase-trello/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/demos/supabase-trello/macos/Runner/Base.lproj/MainMenu.xib b/demos/supabase-trello/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 00000000..80e867a4 --- /dev/null +++ b/demos/supabase-trello/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/supabase-trello/macos/Runner/Configs/AppInfo.xcconfig b/demos/supabase-trello/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 00000000..47fffa43 --- /dev/null +++ b/demos/supabase-trello/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = trelloappclone_flutter + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.trelloappcloneFlutter + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2023 com.example. All rights reserved. diff --git a/demos/supabase-trello/macos/Runner/Configs/Debug.xcconfig b/demos/supabase-trello/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 00000000..36b0fd94 --- /dev/null +++ b/demos/supabase-trello/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/demos/supabase-trello/macos/Runner/Configs/Release.xcconfig b/demos/supabase-trello/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 00000000..dff4f495 --- /dev/null +++ b/demos/supabase-trello/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/demos/supabase-trello/macos/Runner/Configs/Warnings.xcconfig b/demos/supabase-trello/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 00000000..42bcbf47 --- /dev/null +++ b/demos/supabase-trello/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/demos/supabase-trello/macos/Runner/DebugProfile.entitlements b/demos/supabase-trello/macos/Runner/DebugProfile.entitlements new file mode 100644 index 00000000..08c3ab17 --- /dev/null +++ b/demos/supabase-trello/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + + diff --git a/demos/supabase-trello/macos/Runner/Info.plist b/demos/supabase-trello/macos/Runner/Info.plist new file mode 100644 index 00000000..4789daa6 --- /dev/null +++ b/demos/supabase-trello/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/demos/supabase-trello/macos/Runner/MainFlutterWindow.swift b/demos/supabase-trello/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 00000000..3cc05eb2 --- /dev/null +++ b/demos/supabase-trello/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/demos/supabase-trello/macos/Runner/Release.entitlements b/demos/supabase-trello/macos/Runner/Release.entitlements new file mode 100644 index 00000000..779a1789 --- /dev/null +++ b/demos/supabase-trello/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/demos/supabase-trello/macos/RunnerTests/RunnerTests.swift b/demos/supabase-trello/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..5418c9f5 --- /dev/null +++ b/demos/supabase-trello/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import FlutterMacOS +import Cocoa +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/demos/supabase-trello/pubspec.lock b/demos/supabase-trello/pubspec.lock new file mode 100644 index 00000000..d3a96f89 --- /dev/null +++ b/demos/supabase-trello/pubspec.lock @@ -0,0 +1,991 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + app_links: + dependency: transitive + description: + name: app_links + sha256: "433df2e61b10519407475d7f69e470789d23d593f28224c38ba1068597be7950" + url: "https://pub.dev" + source: hosted + version: "6.3.3" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + args: + dependency: transitive + description: + name: args + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + url: "https://pub.dev" + source: hosted + version: "2.6.0" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + fetch_api: + dependency: transitive + description: + name: fetch_api + sha256: "97f46c25b480aad74f7cc2ad7ccba2c5c6f08d008e68f95c1077286ce243d0e6" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + fetch_client: + dependency: transitive + description: + name: fetch_client + sha256: "9666ee14536778474072245ed5cba07db81ae8eb5de3b7bf4a2d1e2c49696092" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "1bbf65dd997458a08b531042ec3794112a6c39c07c37ff22113d2e7e4f81d4e4" + url: "https://pub.dev" + source: hosted + version: "6.2.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" + url: "https://pub.dev" + source: hosted + version: "0.9.4+2" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4" + url: "https://pub.dev" + source: hosted + version: "0.9.3+3" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flare_flutter: + dependency: transitive + description: + name: flare_flutter + sha256: "99d63c60f00fac81249ce6410ee015d7b125c63d8278a30da81edf3317a1f6a0" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: b7c7be5cd9f6ef7a78429cabd2774d3c4af50e79cb2b7593e3d5d763ef95c61b + url: "https://pub.dev" + source: hosted + version: "5.2.1" + flutter_expandable_fab: + dependency: "direct main" + description: + name: flutter_expandable_fab + sha256: "85275279d19faf4fbe5639dc1f139b4555b150e079d056f085601a45688af12c" + url: "https://pub.dev" + source: hosted + version: "2.3.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: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda" + url: "https://pub.dev" + source: hosted + version: "2.0.22" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + functions_client: + dependency: transitive + description: + name: functions_client + sha256: "61597ed93be197b1be6387855e4b760e6aac2355fcfc4df6d20d2b4579982158" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 + url: "https://pub.dev" + source: hosted + version: "6.2.1" + gotrue: + dependency: transitive + description: + name: gotrue + sha256: d6362dff9a54f8c1c372bb137c858b4024c16407324d34e6473e59623c9b9f50 + url: "https://pub.dev" + source: hosted + version: "2.11.1" + 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: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + url: "https://pub.dev" + source: hosted + version: "1.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "8c5abf0dcc24fe6e8e0b4a5c0b51a5cf30cefdf6407a3213dae61edc75a70f56" + url: "https://pub.dev" + source: hosted + version: "0.8.12+12" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "4f0568120c6fcc0aaa04511cb9f9f4d29fc3d0139884b1d06be88dcec7641d6b" + url: "https://pub.dev" + source: hosted + version: "0.8.12+1" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + jwt_decode: + dependency: transitive + description: + name: jwt_decode + sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb + url: "https://pub.dev" + source: hosted + version: "0.3.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + url: "https://pub.dev" + source: hosted + version: "10.0.4" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + logging: + dependency: "direct main" + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + url: "https://pub.dev" + source: hosted + version: "0.8.0" + material_design_icons_flutter: + dependency: "direct main" + description: + name: material_design_icons_flutter + sha256: "6f986b7a51f3ad4c00e33c5c84e8de1bdd140489bbcdc8b66fc1283dad4dea5a" + url: "https://pub.dev" + source: hosted + version: "7.0.7296" + meta: + dependency: transitive + description: + name: meta + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + url: "https://pub.dev" + source: hosted + version: "1.12.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mutex: + dependency: transitive + description: + name: mutex + sha256: "8827da25de792088eb33e572115a5eb0d61d61a3c01acbc8bcbe76ed78f1a1f2" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + path: + dependency: "direct main" + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + 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: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" + url: "https://pub.dev" + source: hosted + version: "2.2.10" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + postgrest: + dependency: transitive + description: + name: postgrest + sha256: "9f759ac497a24839addbed69d9569ea6d51d2e4834c672b8c2a73752fb6945c8" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + powersync: + dependency: "direct main" + description: + path: "../../packages/powersync" + relative: true + source: path + version: "1.10.0" + powersync_core: + dependency: "direct overridden" + description: + path: "../../packages/powersync_core" + relative: true + source: path + version: "1.0.0" + powersync_flutter_libs: + dependency: "direct overridden" + description: + path: "../../packages/powersync_flutter_libs" + relative: true + source: path + version: "0.4.3" + provider: + dependency: "direct main" + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + random_name_generator: + dependency: "direct main" + description: + name: random_name_generator + sha256: "7c5b91d60f68b30e7b4c53006047cab8474f06563f7d0cab70fb409a0cb5ff61" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + realtime_client: + dependency: transitive + description: + name: realtime_client + sha256: "1bfcb7455fdcf15953bf18ac2817634ea5b8f7f350c7e8c9873141a3ee2c3e9c" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + 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: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + 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: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: bb174b3ec2527f9c5f680f73a89af8149dd99782fbb56ea88ad0807c5638f2ed + url: "https://pub.dev" + source: hosted + version: "2.4.7" + sqlite3_flutter_libs: + dependency: transitive + description: + name: sqlite3_flutter_libs + sha256: "73016db8419f019e807b7a5e5fbf2a7bd45c165fed403b8e7681230f3a102785" + url: "https://pub.dev" + source: hosted + version: "0.5.28" + sqlite3_web: + dependency: transitive + description: + name: sqlite3_web + sha256: f22d1dda7a40be0867984f55cdf5c2d599e5f05d3be4a642d78f38b38983f554 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + sqlite_async: + dependency: "direct main" + description: + name: sqlite_async + sha256: d66fb6e6d07c1a834743326c033029f75becbb1fad6823d709f921872abc3d5b + url: "https://pub.dev" + source: hosted + version: "0.11.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + status_alert: + dependency: "direct main" + description: + name: status_alert + sha256: "220ce6c1400d19d817665ac5a87f772e87c901677ac37b93320f4764edf4d23f" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + storage_client: + dependency: transitive + description: + name: storage_client + sha256: d80d34f0aa60e5199646bc301f5750767ee37310c2ecfe8d4bbdd29351e09ab0 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + supabase: + dependency: transitive + description: + name: supabase + sha256: ea3daaf4fc76df9bf42ca00142f8d07b94400943a93d563e87f5575ea78f1c2c + url: "https://pub.dev" + source: hosted + version: "2.6.1" + supabase_flutter: + dependency: "direct main" + description: + name: supabase_flutter + sha256: e8b321706a9b88c60f5fe695305f34917ad2ee55aea0b3af987638c7b0793e13 + url: "https://pub.dev" + source: hosted + version: "2.8.2" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + 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: f0c73347dfcfa5b3db8bc06e1502668265d39c08f310c29bff4e28eea9699f79 + url: "https://pub.dev" + source: hosted + version: "6.3.9" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + url: "https://pub.dev" + source: hosted + version: "6.3.2" + 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: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + 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: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + url: "https://pub.dev" + source: hosted + version: "14.2.1" + web: + dependency: transitive + description: + name: web + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" + url: "https://pub.dev" + source: hosted + version: "5.5.4" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" + yet_another_json_isolate: + dependency: transitive + description: + name: yet_another_json_isolate + sha256: "56155e9e0002cc51ea7112857bbcdc714d4c35e176d43e4d3ee233009ff410c9" + url: "https://pub.dev" + source: hosted + version: "2.0.3" +sdks: + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.22.0" diff --git a/demos/supabase-trello/pubspec.yaml b/demos/supabase-trello/pubspec.yaml new file mode 100644 index 00000000..26fc55e7 --- /dev/null +++ b/demos/supabase-trello/pubspec.yaml @@ -0,0 +1,67 @@ +name: trelloappclone_flutter +description: A Flutter clone of Trello. + +# The following line prevents the package from being accidentally published to +# pub.dev using `pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +version: 1.0.3+1 + +environment: + sdk: ^3.4.0 + +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.5 + google_fonts: ^6.1.0 + flutter_expandable_fab: ^2.0.0 + material_design_icons_flutter: ^7.0.7296 + crypto: ^3.0.3 + provider: ^6.0.5 + status_alert: ^1.0.1 + image_picker: ^1.0.4 + file_picker: ^6.1.1 + random_name_generator: ^1.2.0 + flutter_dotenv: ^5.1.0 + logging: ^1.1.1 + powersync: ^1.10.0 + sqlite_async: ^0.11.0 + path_provider: ^2.0.12 + supabase_flutter: ^2.8.2 + path: ^1.8.2 + +dev_dependencies: + flutter_lints: ^3.0.1 + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - assets/landing.jpg + - assets/trello-logo.png + - .env diff --git a/demos/supabase-trello/sample_workspace.png b/demos/supabase-trello/sample_workspace.png new file mode 100644 index 00000000..7b92ef89 Binary files /dev/null and b/demos/supabase-trello/sample_workspace.png differ diff --git a/demos/supabase-trello/showcase.png b/demos/supabase-trello/showcase.png new file mode 100644 index 00000000..322044b8 Binary files /dev/null and b/demos/supabase-trello/showcase.png differ diff --git a/demos/supabase-trello/sync-rules-0.yaml b/demos/supabase-trello/sync-rules-0.yaml new file mode 100644 index 00000000..6eaeb007 --- /dev/null +++ b/demos/supabase-trello/sync-rules-0.yaml @@ -0,0 +1,19 @@ +#naive version with global access +bucket_definitions: + global: + parameters: | + SELECT FROM trellouser WHERE + trellouser.id = token_parameters.user_id + data: + - SELECT * FROM trellouser + - SELECT * FROM workspace + - SELECT * FROM board + - SELECT * FROM activity + - SELECT * FROM attachment + - SELECT * FROM card + - SELECT * FROM checklist + - SELECT * FROM comment + - SELECT * FROM listboard + - SELECT * FROM member + - SELECT * FROM board_label + - SELECT * FROM card_label diff --git a/demos/supabase-trello/sync-rules-1.yaml b/demos/supabase-trello/sync-rules-1.yaml new file mode 100644 index 00000000..1da8b4c2 --- /dev/null +++ b/demos/supabase-trello/sync-rules-1.yaml @@ -0,0 +1,27 @@ +bucket_definitions: + user_info: + # this allows syncing of all trellouser records so we can lookup users when adding members + data: + - SELECT * FROM trellouser + by_workspace: + # the entities are filtered by workspaceId, thus linked to the workspaces (a) owned by this user, (b) where this user is a member, or (c) which are public + # Note: the quotes for "workspaceId" and "userId" is important, since otherwise postgres does not deal well with non-lowercase identifiers + parameters: + - SELECT id as workspace_id FROM workspace WHERE + workspace."userId" = token_parameters.user_id # OR visibility = "Public" + - SELECT "workspaceId" as workspace_id FROM member WHERE + member."userId" = token_parameters.user_id + - SELECT id as workspace_id FROM workspace WHERE + visibility = "Public" + data: + - SELECT * FROM workspace WHERE workspace.id = bucket.workspace_id + - SELECT * FROM board WHERE board."workspaceId" = bucket.workspace_id + - SELECT * FROM member WHERE member."workspaceId" = bucket.workspace_id + - SELECT * FROM listboard WHERE listboard."workspaceId" = bucket.workspace_id + - SELECT * FROM card WHERE card."workspaceId" = bucket.workspace_id + - SELECT * FROM checklist WHERE checklist."workspaceId" = bucket.workspace_id + - SELECT * FROM activity WHERE activity."workspaceId" = bucket.workspace_id + - SELECT * FROM comment WHERE comment."workspaceId" = bucket.workspace_id + - SELECT * FROM attachment WHERE attachment."workspaceId" = bucket.workspace_id + - SELECT * FROM board_label WHERE board_label."workspaceId" = bucket.workspace_id + - SELECT * FROM card_label WHERE card_label."workspaceId" = bucket.workspace_id diff --git a/demos/supabase-trello/tables.sql b/demos/supabase-trello/tables.sql new file mode 100644 index 00000000..db0beca7 --- /dev/null +++ b/demos/supabase-trello/tables.sql @@ -0,0 +1,332 @@ +-- +-- Class User as table trellouser +-- + +CREATE TABLE "trellouser" ( + "id" uuid not null default gen_random_uuid (), + "name" text, + "email" text NOT NULL, + "password" text NOT NULL +); + +ALTER TABLE ONLY "trellouser" + ADD CONSTRAINT trellouser_pkey PRIMARY KEY (id); + + +-- +-- Class Workspace as table workspace +-- + +CREATE TABLE "workspace" ( + "id" uuid not null default gen_random_uuid (), + "userId" uuid NOT NULL, + "name" text NOT NULL, + "description" text NOT NULL, + "visibility" text NOT NULL +); + +ALTER TABLE ONLY "workspace" + ADD CONSTRAINT workspace_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY "workspace" + ADD CONSTRAINT workspace_fk_0 + FOREIGN KEY("userId") + REFERENCES trellouser(id) + ON DELETE CASCADE; + +-- +-- Class Member as table member +-- + +CREATE TABLE "member" ( + "id" uuid not null default gen_random_uuid (), + "workspaceId" uuid NOT NULL, + "userId" uuid NOT NULL, + "name" text NOT NULL, + "role" text NOT NULL +); + +ALTER TABLE ONLY "member" + ADD CONSTRAINT member_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY "member" + ADD CONSTRAINT member_fk_0 + FOREIGN KEY("workspaceId") + REFERENCES workspace(id) + ON DELETE CASCADE; +ALTER TABLE ONLY "member" + ADD CONSTRAINT member_fk_1 + FOREIGN KEY("userId") + REFERENCES trellouser(id) + ON DELETE CASCADE; + +-- +-- Class Board as table board +-- + +CREATE TABLE "board" ( + "id" uuid not null default gen_random_uuid (), + "workspaceId" uuid NOT NULL, + "userId" uuid NOT NULL, + "name" text NOT NULL, + "description" text, + "visibility" text NOT NULL, + "background" text NOT NULL, + "starred" boolean, + "enableCover" boolean, + "watch" boolean, + "availableOffline" boolean, + "label" text, + "emailAddress" text, + "commenting" integer, + "memberType" integer, + "pinned" boolean, + "selfJoin" boolean, + "close" boolean +); + +ALTER TABLE ONLY "board" + ADD CONSTRAINT board_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY "board" + ADD CONSTRAINT board_fk_0 + FOREIGN KEY("workspaceId") + REFERENCES workspace(id) + ON DELETE CASCADE; +ALTER TABLE ONLY "board" + ADD CONSTRAINT board_fk_1 + FOREIGN KEY("userId") + REFERENCES trellouser(id) + ON DELETE CASCADE; + +-- +-- Class Listboard as table listboard +-- + +CREATE TABLE "listboard" ( + "id" uuid not null default gen_random_uuid (), + "workspaceId" uuid NOT NULL, + "boardId" uuid NOT NULL, + "userId" uuid NOT NULL, + "name" text NOT NULL, + "archived" boolean, + "listOrder" integer +); + +ALTER TABLE ONLY "listboard" + ADD CONSTRAINT listboard_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY "listboard" + ADD CONSTRAINT listboard_fk_0 + FOREIGN KEY("boardId") + REFERENCES board(id) + ON DELETE CASCADE; +ALTER TABLE ONLY "listboard" + ADD CONSTRAINT listboard_fk_1 + FOREIGN KEY("userId") + REFERENCES trellouser(id) + ON DELETE CASCADE; + +-- +-- Class Cardlist as table card +-- + +CREATE TABLE "card" ( + "id" uuid not null default gen_random_uuid (), + "workspaceId" uuid NOT NULL, + "listId" uuid NOT NULL, + "userId" uuid NOT NULL, + "name" text NOT NULL, + "description" text, + "startDate" timestamp without time zone, + "dueDate" timestamp without time zone, + "rank" integer, + "attachment" boolean, + "archived" boolean, + "checklist" boolean, + "comments" boolean +); + +ALTER TABLE ONLY "card" + ADD CONSTRAINT card_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY "card" + ADD CONSTRAINT card_fk_0 + FOREIGN KEY("listId") + REFERENCES listboard(id) + ON DELETE CASCADE; +ALTER TABLE ONLY "card" + ADD CONSTRAINT card_fk_1 + FOREIGN KEY("userId") + REFERENCES trellouser(id) + ON DELETE CASCADE; + +-- +-- Class Attachment as table attachment +-- + +CREATE TABLE "attachment" ( + "id" uuid not null default gen_random_uuid (), + "workspaceId" uuid NOT NULL, + "userId" uuid NOT NULL, + "cardId" uuid NOT NULL, + "attachment" text NOT NULL +); + +ALTER TABLE ONLY "attachment" + ADD CONSTRAINT attachment_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY "attachment" + ADD CONSTRAINT attachment_fk_0 + FOREIGN KEY("userId") + REFERENCES trellouser(id) + ON DELETE CASCADE; +ALTER TABLE ONLY "attachment" + ADD CONSTRAINT attachment_fk_1 + FOREIGN KEY("cardId") + REFERENCES card(id) + ON DELETE CASCADE; + +-- +-- Class Checklist as table checklist +-- + +CREATE TABLE "checklist" ( + "id" uuid not null default gen_random_uuid (), + "workspaceId" uuid NOT NULL, + "cardId" uuid NOT NULL, + "name" text NOT NULL, + "status" boolean NOT NULL +); + +ALTER TABLE ONLY "checklist" + ADD CONSTRAINT checklist_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY "checklist" + ADD CONSTRAINT checklist_fk_0 + FOREIGN KEY("cardId") + REFERENCES card(id) + ON DELETE CASCADE; + +-- +-- Class Comment as table comment +-- + +CREATE TABLE "comment" ( + "id" uuid not null default gen_random_uuid (), + "workspaceId" uuid NOT NULL, + "cardId" uuid NOT NULL, + "userId" uuid NOT NULL, + "description" text NOT NULL +); + +ALTER TABLE ONLY "comment" + ADD CONSTRAINT comment_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY "comment" + ADD CONSTRAINT comment_fk_0 + FOREIGN KEY("cardId") + REFERENCES card(id) + ON DELETE CASCADE; +ALTER TABLE ONLY "comment" + ADD CONSTRAINT comment_fk_1 + FOREIGN KEY("userId") + REFERENCES trellouser(id) + ON DELETE CASCADE; + +-- +-- Class Activity as table activity +-- + +CREATE TABLE "activity" ( + "id" uuid not null default gen_random_uuid (), + "workspaceId" uuid NOT NULL, + "boardId" uuid, + "userId" uuid NOT NULL, + "cardId" uuid, + "description" text NOT NULL, + "dateCreated" timestamp without time zone NOT NULL +); + +ALTER TABLE ONLY "activity" + ADD CONSTRAINT activity_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY "activity" + ADD CONSTRAINT activity_fk_0 + FOREIGN KEY("boardId") + REFERENCES board(id) + ON DELETE CASCADE; +ALTER TABLE ONLY "activity" + ADD CONSTRAINT activity_fk_1 + FOREIGN KEY("userId") + REFERENCES trellouser(id) + ON DELETE CASCADE; +ALTER TABLE ONLY "activity" + ADD CONSTRAINT activity_fk_2 + FOREIGN KEY("cardId") + REFERENCES card(id) + ON DELETE CASCADE; + +-- +-- Class Board Labels as table board_labels +-- + +CREATE TABLE "board_label" ( + "id" uuid not null default gen_random_uuid (), + "boardId" uuid NOT NULL, + "workspaceId" uuid NOT NULL, + "title" text NOT NULL, + "color" text NOT NULL, + "dateCreated" timestamp without time zone NOT NULL +); + +ALTER TABLE ONLY "board_label" + ADD CONSTRAINT board_label_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY "board_label" + ADD CONSTRAINT board_label_fk_0 + FOREIGN KEY("boardId") + REFERENCES board(id) + ON DELETE CASCADE; +ALTER TABLE ONLY "board_label" + ADD CONSTRAINT board_label_fk_1 + FOREIGN KEY("workspaceId") + REFERENCES workspace(id) + ON DELETE CASCADE; + +-- +-- Class Card Labels as table card_labels +-- + +CREATE TABLE "card_label" ( + "id" uuid not null default gen_random_uuid (), + "boardLabelId" uuid NOT NULL, + "workspaceId" uuid NOT NULL, + "boardId" uuid NOT NULL, + "cardId" uuid NOT NULL, + "dateCreated" timestamp without time zone NOT NULL +); + +ALTER TABLE ONLY "card_label" + ADD CONSTRAINT card_label_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY "card_label" + ADD CONSTRAINT card_label_fk_0 + FOREIGN KEY("boardLabelId") + REFERENCES board_label(id) + ON DELETE CASCADE; +ALTER TABLE ONLY "card_label" + ADD CONSTRAINT card_label_fk_1 + FOREIGN KEY("cardId") + REFERENCES card(id) + ON DELETE CASCADE; +ALTER TABLE ONLY "card_label" + ADD CONSTRAINT card_label_fk_2 + FOREIGN KEY("workspaceId") + REFERENCES workspace(id) + ON DELETE CASCADE; +ALTER TABLE ONLY "card_label" + ADD CONSTRAINT card_label_fk_3 + FOREIGN KEY("boardId") + REFERENCES board(id) + ON DELETE CASCADE; \ No newline at end of file diff --git a/demos/supabase-trello/test/widget_test.dart b/demos/supabase-trello/test/widget_test.dart new file mode 100644 index 00000000..ac1458c1 --- /dev/null +++ b/demos/supabase-trello/test/widget_test.dart @@ -0,0 +1,15 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. 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:PROJECTNAME_flutter/main.dart'; + +void main() { + // Add your app tests here +} diff --git a/demos/supabase-trello/web/favicon.png b/demos/supabase-trello/web/favicon.png new file mode 100644 index 00000000..8aaa46ac Binary files /dev/null and b/demos/supabase-trello/web/favicon.png differ diff --git a/demos/supabase-trello/web/icons/Icon-192.png b/demos/supabase-trello/web/icons/Icon-192.png new file mode 100644 index 00000000..b749bfef Binary files /dev/null and b/demos/supabase-trello/web/icons/Icon-192.png differ diff --git a/demos/supabase-trello/web/icons/Icon-512.png b/demos/supabase-trello/web/icons/Icon-512.png new file mode 100644 index 00000000..88cfd48d Binary files /dev/null and b/demos/supabase-trello/web/icons/Icon-512.png differ diff --git a/demos/supabase-trello/web/icons/Icon-maskable-192.png b/demos/supabase-trello/web/icons/Icon-maskable-192.png new file mode 100644 index 00000000..eb9b4d76 Binary files /dev/null and b/demos/supabase-trello/web/icons/Icon-maskable-192.png differ diff --git a/demos/supabase-trello/web/icons/Icon-maskable-512.png b/demos/supabase-trello/web/icons/Icon-maskable-512.png new file mode 100644 index 00000000..d69c5669 Binary files /dev/null and b/demos/supabase-trello/web/icons/Icon-maskable-512.png differ diff --git a/demos/supabase-trello/web/index.html b/demos/supabase-trello/web/index.html new file mode 100644 index 00000000..3b048eea --- /dev/null +++ b/demos/supabase-trello/web/index.html @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + trelloappclone_flutter + + + + + + + + + + diff --git a/demos/supabase-trello/web/manifest.json b/demos/supabase-trello/web/manifest.json new file mode 100644 index 00000000..a2531a89 --- /dev/null +++ b/demos/supabase-trello/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "trelloappclone_flutter", + "short_name": "trelloappclone_flutter", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/demos/supabase-trello/windows/.gitignore b/demos/supabase-trello/windows/.gitignore new file mode 100644 index 00000000..d492d0d9 --- /dev/null +++ b/demos/supabase-trello/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/demos/supabase-trello/windows/CMakeLists.txt b/demos/supabase-trello/windows/CMakeLists.txt new file mode 100644 index 00000000..c5b3a9b4 --- /dev/null +++ b/demos/supabase-trello/windows/CMakeLists.txt @@ -0,0 +1,102 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(trelloappclone_flutter LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "trelloappclone_flutter") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/demos/supabase-trello/windows/flutter/CMakeLists.txt b/demos/supabase-trello/windows/flutter/CMakeLists.txt new file mode 100644 index 00000000..930d2071 --- /dev/null +++ b/demos/supabase-trello/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/demos/supabase-trello/windows/flutter/generated_plugin_registrant.cc b/demos/supabase-trello/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..2a044a64 --- /dev/null +++ b/demos/supabase-trello/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,26 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + AppLinksPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksPluginCApi")); + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); + PowersyncFlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PowersyncFlutterLibsPlugin")); + Sqlite3FlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/demos/supabase-trello/windows/flutter/generated_plugin_registrant.h b/demos/supabase-trello/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..dc139d85 --- /dev/null +++ b/demos/supabase-trello/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/demos/supabase-trello/windows/flutter/generated_plugins.cmake b/demos/supabase-trello/windows/flutter/generated_plugins.cmake new file mode 100644 index 00000000..18c83191 --- /dev/null +++ b/demos/supabase-trello/windows/flutter/generated_plugins.cmake @@ -0,0 +1,28 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + app_links + file_selector_windows + powersync_flutter_libs + sqlite3_flutter_libs + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/demos/supabase-trello/windows/runner/CMakeLists.txt b/demos/supabase-trello/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..394917c0 --- /dev/null +++ b/demos/supabase-trello/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/demos/supabase-trello/windows/runner/Runner.rc b/demos/supabase-trello/windows/runner/Runner.rc new file mode 100644 index 00000000..58312f4e --- /dev/null +++ b/demos/supabase-trello/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "trelloappclone_flutter" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "trelloappclone_flutter" "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "trelloappclone_flutter.exe" "\0" + VALUE "ProductName", "trelloappclone_flutter" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/demos/supabase-trello/windows/runner/flutter_window.cpp b/demos/supabase-trello/windows/runner/flutter_window.cpp new file mode 100644 index 00000000..b25e363e --- /dev/null +++ b/demos/supabase-trello/windows/runner/flutter_window.cpp @@ -0,0 +1,66 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/demos/supabase-trello/windows/runner/flutter_window.h b/demos/supabase-trello/windows/runner/flutter_window.h new file mode 100644 index 00000000..6da0652f --- /dev/null +++ b/demos/supabase-trello/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/demos/supabase-trello/windows/runner/main.cpp b/demos/supabase-trello/windows/runner/main.cpp new file mode 100644 index 00000000..8dfa412e --- /dev/null +++ b/demos/supabase-trello/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"trelloappclone_flutter", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/demos/supabase-trello/windows/runner/resource.h b/demos/supabase-trello/windows/runner/resource.h new file mode 100644 index 00000000..66a65d1e --- /dev/null +++ b/demos/supabase-trello/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/demos/supabase-trello/windows/runner/resources/app_icon.ico b/demos/supabase-trello/windows/runner/resources/app_icon.ico new file mode 100644 index 00000000..c04e20ca Binary files /dev/null and b/demos/supabase-trello/windows/runner/resources/app_icon.ico differ diff --git a/demos/supabase-trello/windows/runner/runner.exe.manifest b/demos/supabase-trello/windows/runner/runner.exe.manifest new file mode 100644 index 00000000..a42ea768 --- /dev/null +++ b/demos/supabase-trello/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/demos/supabase-trello/windows/runner/utils.cpp b/demos/supabase-trello/windows/runner/utils.cpp new file mode 100644 index 00000000..b2b08734 --- /dev/null +++ b/demos/supabase-trello/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length <= 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/demos/supabase-trello/windows/runner/utils.h b/demos/supabase-trello/windows/runner/utils.h new file mode 100644 index 00000000..3879d547 --- /dev/null +++ b/demos/supabase-trello/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/demos/supabase-trello/windows/runner/win32_window.cpp b/demos/supabase-trello/windows/runner/win32_window.cpp new file mode 100644 index 00000000..60608d0f --- /dev/null +++ b/demos/supabase-trello/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/demos/supabase-trello/windows/runner/win32_window.h b/demos/supabase-trello/windows/runner/win32_window.h new file mode 100644 index 00000000..e901dde6 --- /dev/null +++ b/demos/supabase-trello/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_