diff --git a/examples/realtime/flutter-figma-clone/.gitignore b/examples/realtime/flutter-figma-clone/.gitignore new file mode 100644 index 00000000000..29a3a5017f0 --- /dev/null +++ b/examples/realtime/flutter-figma-clone/.gitignore @@ -0,0 +1,43 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/examples/realtime/flutter-figma-clone/.metadata b/examples/realtime/flutter-figma-clone/.metadata new file mode 100644 index 00000000000..5e4a304849c --- /dev/null +++ b/examples/realtime/flutter-figma-clone/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "b0366e0a3f089e15fd89c97604ab402fe26b724c" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c + base_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c + - platform: android + create_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c + base_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c + - platform: ios + create_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c + base_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c + - platform: linux + create_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c + base_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c + - platform: macos + create_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c + base_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c + - platform: web + create_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c + base_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c + - platform: windows + create_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c + base_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c + + # 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/examples/realtime/flutter-figma-clone/README.md b/examples/realtime/flutter-figma-clone/README.md new file mode 100644 index 00000000000..27b5ddab133 --- /dev/null +++ b/examples/realtime/flutter-figma-clone/README.md @@ -0,0 +1,19 @@ +# Flutter Figma Clone + +![Supabase Figma Clone](https://raw.githubusercontent.com/dshukertjr/examples/main/.github/images/design.png 'Supabase Figma Clone') + +First of all, Figma clone is an over statement 😂 This app demonstrates the real-time communication capabilities of [Supabase](https://supabase.com) by creating an interactive design board where you can draw some shapes and move them around while seeing each other's cursors. + +- Cursor position sharing in real-time +- Drawing shapes in real-time + +## Run locally + +- Run `supabase start` while docker running. If you don't have Supabase CLI yet, you can get it [here](https://supabase.com/docs/guides/cli/getting-started). +- Run Flutter locally. The app should work on any platform, but is not optimized for mobile. + +## Resources + +- [Supabase](https://supabase.com/) +- [Supabase Broadcast](https://supabase.com/docs/guides/realtime/broadcast) +- [Flutter Tutorial: building a Flutter chat app](https://supabase.com/blog/flutter-tutorial-building-a-chat-app) diff --git a/examples/realtime/flutter-figma-clone/analysis_options.yaml b/examples/realtime/flutter-figma-clone/analysis_options.yaml new file mode 100644 index 00000000000..0d2902135ca --- /dev/null +++ b/examples/realtime/flutter-figma-clone/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/examples/realtime/flutter-figma-clone/lib/canvas/canvas_object.dart b/examples/realtime/flutter-figma-clone/lib/canvas/canvas_object.dart new file mode 100644 index 00000000000..e3a14aa3d16 --- /dev/null +++ b/examples/realtime/flutter-figma-clone/lib/canvas/canvas_object.dart @@ -0,0 +1,242 @@ +import 'dart:convert'; +import 'dart:math'; +import 'dart:ui'; + +import 'package:uuid/uuid.dart'; + +/// Handy extension method to create random colors +extension RandomColor on Color { + static Color getRandom() { + return Color((Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0); + } + + /// Quick and dirty method to create a random color from the userID + static Color getRandomFromId(String id) { + final seed = utf8.encode(id).reduce((value, element) => value + element); + return Color((Random(seed).nextDouble() * 0xFFFFFF).toInt()) + .withOpacity(1.0); + } +} + +/// Objects that are being synced in realtime over broadcast +/// +/// Includes mouse cursor and design objects +abstract class SyncedObject { + /// UUID unique identifier of the object + final String id; + + factory SyncedObject.fromJson(Map json) { + final objectType = json['object_type']; + if (objectType == UserCursor.type) { + return UserCursor.fromJson(json); + } else { + return CanvasObject.fromJson(json); + } + } + + SyncedObject({ + required this.id, + }); + + Map toJson(); +} + +/// Data model for the cursors displayed on the canvas. +class UserCursor extends SyncedObject { + static String type = 'cursor'; + + final Offset position; + final Color color; + + UserCursor({ + required super.id, + required this.position, + }) : color = RandomColor.getRandomFromId(id); + + UserCursor.fromJson(Map json) + : position = Offset(json['position']['x'], json['position']['y']), + color = RandomColor.getRandomFromId(json['id']), + super(id: json['id']); + + @override + Map toJson() { + return { + 'object_type': type, + 'id': id, + 'position': { + 'x': position.dx, + 'y': position.dy, + } + }; + } +} + +/// Base model for any design objects displayed on the canvas. +abstract class CanvasObject extends SyncedObject { + final Color color; + + CanvasObject({ + required super.id, + required this.color, + }); + + factory CanvasObject.fromJson(Map json) { + if (json['object_type'] == Circle.type) { + return Circle.fromJson(json); + } else if (json['object_type'] == Rectangle.type) { + return Rectangle.fromJson(json); + } else { + throw UnimplementedError('Unknown object_type: ${json['object_type']}'); + } + } + + /// Whether or not the object intersects with the given point. + bool intersectsWith(Offset point); + + CanvasObject copyWith(); + + /// Moves the object to a new position + CanvasObject move(Offset delta); +} + +/// Circle displayed on the canvas. +class Circle extends CanvasObject { + static String type = 'circle'; + + final Offset center; + final double radius; + + Circle({ + required super.id, + required super.color, + required this.radius, + required this.center, + }); + + Circle.fromJson(Map json) + : radius = json['radius'], + center = Offset(json['center']['x'], json['center']['y']), + super(id: json['id'], color: Color(json['color'])); + + /// Constructor to be used when first starting to draw the object on the canvas + Circle.createNew(this.center) + : radius = 0, + super(id: const Uuid().v4(), color: RandomColor.getRandom()); + + @override + Map toJson() { + return { + 'object_type': type, + 'id': id, + 'color': color.value, + 'center': { + 'x': center.dx, + 'y': center.dy, + }, + 'radius': radius, + }; + } + + @override + Circle copyWith({ + double? radius, + Offset? center, + Color? color, + }) { + return Circle( + radius: radius ?? this.radius, + center: center ?? this.center, + id: id, + color: color ?? this.color, + ); + } + + @override + bool intersectsWith(Offset point) { + final centerToPointerDistance = (point - center).distance; + return radius > centerToPointerDistance; + } + + @override + Circle move(Offset delta) { + return copyWith(center: center + delta); + } +} + +/// Rectangle displayed on the canvas. +class Rectangle extends CanvasObject { + static String type = 'rectangle'; + + final Offset topLeft; + final Offset bottomRight; + + Rectangle({ + required super.id, + required super.color, + required this.topLeft, + required this.bottomRight, + }); + + Rectangle.fromJson(Map json) + : bottomRight = + Offset(json['bottom_right']['x'], json['bottom_right']['y']), + topLeft = Offset(json['top_left']['x'], json['top_left']['y']), + super(id: json['id'], color: Color(json['color'])); + + /// Constructor to be used when first starting to draw the object on the canvas + Rectangle.createNew(Offset startingPoint) + : topLeft = startingPoint, + bottomRight = startingPoint, + super(color: RandomColor.getRandom(), id: const Uuid().v4()); + + @override + Map toJson() { + return { + 'object_type': type, + 'id': id, + 'color': color.value, + 'top_left': { + 'x': topLeft.dx, + 'y': topLeft.dy, + }, + 'bottom_right': { + 'x': bottomRight.dx, + 'y': bottomRight.dy, + }, + }; + } + + @override + Rectangle copyWith({ + Offset? topLeft, + Offset? bottomRight, + Color? color, + }) { + return Rectangle( + topLeft: topLeft ?? this.topLeft, + id: id, + bottomRight: bottomRight ?? this.bottomRight, + color: color ?? this.color, + ); + } + + @override + bool intersectsWith(Offset point) { + final minX = min(topLeft.dx, bottomRight.dx); + final maxX = max(topLeft.dx, bottomRight.dx); + final minY = min(topLeft.dy, bottomRight.dy); + final maxY = max(topLeft.dy, bottomRight.dy); + return minX < point.dx && + point.dx < maxX && + minY < point.dy && + point.dy < maxY; + } + + @override + Rectangle move(Offset delta) { + return copyWith( + topLeft: topLeft + delta, + bottomRight: bottomRight + delta, + ); + } +} diff --git a/examples/realtime/flutter-figma-clone/lib/canvas/canvas_page.dart b/examples/realtime/flutter-figma-clone/lib/canvas/canvas_page.dart new file mode 100644 index 00000000000..4ca95c03110 --- /dev/null +++ b/examples/realtime/flutter-figma-clone/lib/canvas/canvas_page.dart @@ -0,0 +1,263 @@ +import 'dart:math'; + +import 'package:canvas/canvas/canvas_object.dart'; +import 'package:canvas/canvas/canvas_painter.dart'; +import 'package:canvas/main.dart'; +import 'package:canvas/utils/constants.dart'; +import 'package:flutter/material.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:uuid/uuid.dart'; + +/// Different input modes users can perform +enum _DrawMode { + /// Mode to move around existing objects + pointer(iconData: Icons.pan_tool_alt_outlined), + + /// Mode to draw circles + circle(iconData: Icons.circle_outlined), + + /// Mode to draw rectangles + rectangle(iconData: Icons.rectangle_outlined); + + const _DrawMode({required this.iconData}); + + /// Icon used in the IconButton to toggle the mode + final IconData iconData; +} + +/// Interactive art board page to draw and collaborate with other users. +class CanvasPage extends StatefulWidget { + const CanvasPage({super.key}); + + @override + State createState() => _CanvasPageState(); +} + +class _CanvasPageState extends State { + /// Holds the cursor information of other users + final Map _userCursors = {}; + + /// Holds the list of objects drawn on the canvas + final Map _canvasObjects = {}; + + /// Supabase realtime channel to communicate to other clients + late final RealtimeChannel _canvasChanel; + + /// Randomly generated UUID for the user + late final String _myId; + + /// Whether the user is using the pointer to move things around, or in drawing mode. + _DrawMode _currentMode = _DrawMode.pointer; + + /// A single Canvas object that is being drawn by the user if any. + String? _currentlyDrawingObjectId; + + /// The point where the pan started + Offset? _panStartPoint; + + /// Cursor position of the user. + Offset _cursorPosition = const Offset(0, 0); + + @override + void initState() { + super.initState(); + _initialize(); + } + + Future _initialize() async { + // Generate a random UUID for the user. + // We could replace this with Supabase auth user ID if we want to make it + // more like Figma. + _myId = const Uuid().v4(); + + // Start listening to broadcast messages to display other users' cursors and objects. + _canvasChanel = supabase + .channel(Constants.channelName) + .onBroadcast( + event: Constants.broadcastEventName, + callback: (payload) { + final cursor = UserCursor.fromJson(payload['cursor']); + _userCursors[cursor.id] = cursor; + + if (payload['object'] != null) { + final object = CanvasObject.fromJson(payload['object']); + _canvasObjects[object.id] = object; + } + setState(() {}); + }) + .subscribe(); + + final initialData = await supabase + .from('canvas_objects') + .select() + .order('created_at', ascending: true); + for (final canvasObjectData in initialData) { + final canvasObject = CanvasObject.fromJson(canvasObjectData['object']); + _canvasObjects[canvasObject.id] = canvasObject; + } + setState(() {}); + } + + /// Syncs the user's cursor position and the currently drawing object with + /// other users. + Future _syncCanvasObject(Offset cursorPosition) { + final myCursor = UserCursor( + position: cursorPosition, + id: _myId, + ); + return _canvasChanel.sendBroadcastMessage( + event: Constants.broadcastEventName, + payload: { + 'cursor': myCursor.toJson(), + if (_currentlyDrawingObjectId != null) + 'object': _canvasObjects[_currentlyDrawingObjectId]?.toJson(), + }, + ); + } + + /// Called when pan starts. + /// + /// For [_DrawMode.pointer], it will find the first object under the cursor. + /// + /// For other draw modes, it will start drawing the respective canvas objects. + void _onPanDown(DragDownDetails details) { + switch (_currentMode) { + case _DrawMode.pointer: + // Loop through the canvas objects to find if there are any + // that intersects with the current mouse position. + for (final canvasObject in _canvasObjects.values.toList().reversed) { + if (canvasObject.intersectsWith(details.globalPosition)) { + _currentlyDrawingObjectId = canvasObject.id; + break; + } + } + break; + case _DrawMode.circle: + final newObject = Circle.createNew(details.globalPosition); + _canvasObjects[newObject.id] = newObject; + _currentlyDrawingObjectId = newObject.id; + break; + case _DrawMode.rectangle: + final newObject = Rectangle.createNew(details.globalPosition); + _canvasObjects[newObject.id] = newObject; + _currentlyDrawingObjectId = newObject.id; + break; + } + _cursorPosition = details.globalPosition; + _panStartPoint = details.globalPosition; + setState(() {}); + } + + /// Called when the user clicks and drags the canvas. + /// + /// Performs different actions depending on the current mode. + void _onPanUpdate(DragUpdateDetails details) { + switch (_currentMode) { + // Moves the object to [details.delta] amount. + case _DrawMode.pointer: + if (_currentlyDrawingObjectId != null) { + _canvasObjects[_currentlyDrawingObjectId!] = + _canvasObjects[_currentlyDrawingObjectId!]!.move(details.delta); + } + break; + + // Updates the size of the Circle + case _DrawMode.circle: + final currentlyDrawingCircle = + _canvasObjects[_currentlyDrawingObjectId!]! as Circle; + _canvasObjects[_currentlyDrawingObjectId!] = + currentlyDrawingCircle.copyWith( + center: (details.globalPosition + _panStartPoint!) / 2, + radius: min((details.globalPosition.dx - _panStartPoint!.dx).abs(), + (details.globalPosition.dy - _panStartPoint!.dy).abs()) / + 2, + ); + break; + + // Updates the size of the rectangle + case _DrawMode.rectangle: + _canvasObjects[_currentlyDrawingObjectId!] = + (_canvasObjects[_currentlyDrawingObjectId!] as Rectangle).copyWith( + bottomRight: details.globalPosition, + ); + break; + } + + if (_currentlyDrawingObjectId != null) { + setState(() {}); + } + _cursorPosition = details.globalPosition; + _syncCanvasObject(_cursorPosition); + } + + void onPanEnd(DragEndDetails _) async { + if (_currentlyDrawingObjectId != null) { + _syncCanvasObject(_cursorPosition); + } + + final drawnObjectId = _currentlyDrawingObjectId; + + setState(() { + _panStartPoint = null; + _currentlyDrawingObjectId = null; + }); + + // Save whatever was drawn to Supabase DB + if (drawnObjectId == null) { + return; + } + await supabase.from('canvas_objects').upsert({ + 'id': drawnObjectId, + 'object': _canvasObjects[drawnObjectId]!.toJson(), + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: MouseRegion( + onHover: (event) { + _syncCanvasObject(event.position); + }, + child: Stack( + children: [ + // The main canvas + GestureDetector( + onPanDown: _onPanDown, + onPanUpdate: _onPanUpdate, + onPanEnd: onPanEnd, + child: CustomPaint( + size: Size(MediaQuery.of(context).size.width, + MediaQuery.of(context).size.height), + painter: CanvasPainter( + userCursors: _userCursors, + canvasObjects: _canvasObjects, + ), + ), + ), + + // Buttons to change the current mode. + Positioned( + top: 0, + left: 0, + child: Row( + children: _DrawMode.values + .map((mode) => IconButton( + iconSize: 48, + onPressed: () { + setState(() { + _currentMode = mode; + }); + }, + icon: Icon(mode.iconData), + color: _currentMode == mode ? Colors.green : null, + )) + .toList(), + ), + ), + ], + ), + ), + ); + } +} diff --git a/examples/realtime/flutter-figma-clone/lib/canvas/canvas_painter.dart b/examples/realtime/flutter-figma-clone/lib/canvas/canvas_painter.dart new file mode 100644 index 00000000000..7bb635b0e43 --- /dev/null +++ b/examples/realtime/flutter-figma-clone/lib/canvas/canvas_painter.dart @@ -0,0 +1,48 @@ +import 'package:canvas/canvas/canvas_object.dart'; +import 'package:flutter/material.dart'; + +class CanvasPainter extends CustomPainter { + final Map userCursors; + final Map canvasObjects; + + CanvasPainter({ + required this.userCursors, + required this.canvasObjects, + }); + + @override + void paint(Canvas canvas, Size size) { + // Draw each canvas objects + for (final canvasObject in canvasObjects.values) { + if (canvasObject is Circle) { + final position = canvasObject.center; + final radius = canvasObject.radius; + canvas.drawCircle( + position, radius, Paint()..color = canvasObject.color); + } else if (canvasObject is Rectangle) { + final position = canvasObject.topLeft; + final bottomRight = canvasObject.bottomRight; + canvas.drawRect( + Rect.fromLTRB( + position.dx, position.dy, bottomRight.dx, bottomRight.dy), + Paint()..color = canvasObject.color); + } + } + + // Draw the cursors + for (final userCursor in userCursors.values) { + final position = userCursor.position; + canvas.drawPath( + Path() + ..moveTo(position.dx, position.dy) + ..lineTo(position.dx + 14.29, position.dy + 44.84) + ..lineTo(position.dx + 20.35, position.dy + 25.93) + ..lineTo(position.dx + 39.85, position.dy + 24.51) + ..lineTo(position.dx, position.dy), + Paint()..color = userCursor.color); + } + } + + @override + bool shouldRepaint(oldPainter) => true; +} diff --git a/examples/realtime/flutter-figma-clone/lib/main.dart b/examples/realtime/flutter-figma-clone/lib/main.dart new file mode 100644 index 00000000000..0061d63f1ab --- /dev/null +++ b/examples/realtime/flutter-figma-clone/lib/main.dart @@ -0,0 +1,27 @@ +import 'package:canvas/canvas/canvas_page.dart'; +import 'package:flutter/material.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +void main() async { + Supabase.initialize( + url: 'http://127.0.0.1:54321', + anonKey: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0', + ); + runApp(const MyApp()); +} + +final supabase = Supabase.instance.client; + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + title: 'Flutter Demo', + debugShowCheckedModeBanner: false, + home: CanvasPage(), + ); + } +} diff --git a/examples/realtime/flutter-figma-clone/lib/utils/constants.dart b/examples/realtime/flutter-figma-clone/lib/utils/constants.dart new file mode 100644 index 00000000000..8a79a86a65f --- /dev/null +++ b/examples/realtime/flutter-figma-clone/lib/utils/constants.dart @@ -0,0 +1,7 @@ +abstract class Constants { + /// Name of the realtime channel + static const String channelName = 'canvas'; + + /// Name of the broadcast event + static const String broadcastEventName = 'canvas'; +} diff --git a/examples/realtime/flutter-figma-clone/pubspec.lock b/examples/realtime/flutter-figma-clone/pubspec.lock new file mode 100644 index 00000000000..30e27950e3d --- /dev/null +++ b/examples/realtime/flutter-figma-clone/pubspec.lock @@ -0,0 +1,594 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + app_links: + dependency: transitive + description: + name: app_links + sha256: "4e392b5eba997df356ca6021f28431ce1cfeb16758699553a94b13add874a3bb" + url: "https://pub.dev" + source: hosted + version: "3.5.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" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + url: "https://pub.dev" + source: hosted + version: "2.0.3" + 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: "9a0ab83a525c8691a6724746e642de755a299afa04158807787364cd9e718001" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + gotrue: + dependency: transitive + description: + name: gotrue + sha256: b6c30fa49dfb243f9c61214651ffe2611704b46fddb520125b5f45610cb9a118 + url: "https://pub.dev" + source: hosted + version: "2.4.0" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" + hive: + dependency: transitive + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + hive_flutter: + dependency: transitive + description: + name: hive_flutter + sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc + url: "https://pub.dev" + source: hosted + version: "1.1.0" + http: + dependency: transitive + description: + name: http + sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + jwt_decode: + dependency: transitive + description: + name: jwt_decode + sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb + url: "https://pub.dev" + source: hosted + version: "0.3.1" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" + source: hosted + version: "0.12.16" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + meta: + dependency: transitive + description: + name: meta + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + url: "https://pub.dev" + source: hosted + version: "1.10.0" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + path: + dependency: transitive + description: + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + url: "https://pub.dev" + source: hosted + version: "2.3.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: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + 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: "748ebffffb60b4eaa270955dcf3742a19a2b315344c41ff1b4a0ebcd322b5181" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + realtime_client: + dependency: transitive + description: + name: realtime_client + sha256: "5831636c19802ba936093a35a7c5b745b130e268fa052e84b4b5290139d2ae03" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + retry: + dependency: transitive + description: + name: retry + sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" + url: "https://pub.dev" + source: hosted + version: "2.3.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + url: "https://pub.dev" + source: hosted + version: "2.3.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + storage_client: + dependency: transitive + description: + name: storage_client + sha256: b49ff2e1e6738c0ef445546d6ec77040829947f0c7ef0b115acb125656127c83 + url: "https://pub.dev" + source: hosted + version: "2.0.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: "976baea81ed75e3b5925db13fe0bfed79440e3b2d8d0753e2f58ab402457b467" + url: "https://pub.dev" + source: hosted + version: "2.0.5" + supabase_flutter: + dependency: "direct main" + description: + name: supabase_flutter + sha256: "4bf88de6ff3c7c350863c907f02f02db5f5a112a826c5a6c02de69d52898bf30" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: e9aa5ea75c84cf46b3db4eea212523591211c3cf2e13099ee4ec147f54201c86 + url: "https://pub.dev" + source: hosted + version: "6.2.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: c0766a55ab42cefaa728cabc951e82919ab41a3a4fee0aaa96176ca82da8cc51 + url: "https://pub.dev" + source: hosted + version: "6.2.1" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "46b81e3109cbb2d6b81702ad3077540789a3e74e22795eb9f0b7d494dbaa72ea" + url: "https://pub.dev" + source: hosted + version: "6.2.2" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + url: "https://pub.dev" + source: hosted + version: "3.1.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "4aca1e060978e19b2998ee28503f40b5ba6226819c2b5e3e4d1821e8ccd92198" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + url: "https://pub.dev" + source: hosted + version: "2.2.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 + url: "https://pub.dev" + source: hosted + version: "4.3.3" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + web: + dependency: transitive + description: + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + url: "https://pub.dev" + source: hosted + version: "0.3.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + win32: + dependency: transitive + description: + name: win32 + sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + url: "https://pub.dev" + source: hosted + version: "5.2.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" + yet_another_json_isolate: + dependency: transitive + description: + name: yet_another_json_isolate + sha256: e727502a2640d65b4b8a8a6cb48af9dd0cbe644ba4b3ee667c7f4afa0c1d6069 + url: "https://pub.dev" + source: hosted + version: "2.0.0" +sdks: + dart: ">=3.2.3 <4.0.0" + flutter: ">=3.16.0" diff --git a/examples/realtime/flutter-figma-clone/pubspec.yaml b/examples/realtime/flutter-figma-clone/pubspec.yaml new file mode 100644 index 00000000000..4f07284c841 --- /dev/null +++ b/examples/realtime/flutter-figma-clone/pubspec.yaml @@ -0,0 +1,25 @@ +name: canvas +description: 'An open source Figma alternative' + +publish_to: 'none' + +version: 1.0.0+1 + +environment: + sdk: '>=3.2.3 <4.0.0' + +dependencies: + flutter: + sdk: flutter + + supabase_flutter: ^2.3.0 + uuid: ^4.3.3 + +dev_dependencies: + flutter_test: + sdk: flutter + + flutter_lints: ^2.0.0 + +flutter: + uses-material-design: true diff --git a/examples/realtime/flutter-figma-clone/supabase/.gitignore b/examples/realtime/flutter-figma-clone/supabase/.gitignore new file mode 100644 index 00000000000..a3ad88055b7 --- /dev/null +++ b/examples/realtime/flutter-figma-clone/supabase/.gitignore @@ -0,0 +1,4 @@ +# Supabase +.branches +.temp +.env diff --git a/examples/realtime/flutter-figma-clone/supabase/config.toml b/examples/realtime/flutter-figma-clone/supabase/config.toml new file mode 100644 index 00000000000..5853703948d --- /dev/null +++ b/examples/realtime/flutter-figma-clone/supabase/config.toml @@ -0,0 +1,149 @@ +# A string used to distinguish different Supabase projects on the same host. Defaults to the +# working directory name when running `supabase init`. +project_id = "design" + +[api] +enabled = true +# Port to use for the API URL. +port = 54321 +# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API +# endpoints. public and storage are always included. +schemas = ["public", "storage", "graphql_public"] +# Extra schemas to add to the search_path of every request. public is always included. +extra_search_path = ["public", "extensions"] +# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size +# for accidental or malicious requests. +max_rows = 1000 + +[db] +# Port to use for the local database URL. +port = 54322 +# Port used by db diff command to initialize the shadow database. +shadow_port = 54320 +# The database major version to use. This has to be the same as your remote database's. Run `SHOW +# server_version;` on the remote database to check. +major_version = 15 + +[db.pooler] +enabled = false +# Port to use for the local connection pooler. +port = 54329 +# Specifies when a server connection can be reused by other clients. +# Configure one of the supported pooler modes: `transaction`, `session`. +pool_mode = "transaction" +# How many server connections to allow per user/database pair. +default_pool_size = 20 +# Maximum number of client connections allowed. +max_client_conn = 100 + +[realtime] +enabled = true +# Bind realtime via either IPv4 or IPv6. (default: IPv6) +# ip_version = "IPv6" + +[studio] +enabled = true +# Port to use for Supabase Studio. +port = 54323 +# External URL of the API server that frontend connects to. +api_url = "http://127.0.0.1" + +# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they +# are monitored, and you can view the emails that would have been sent from the web interface. +[inbucket] +enabled = true +# Port to use for the email testing server web interface. +port = 54324 +# Uncomment to expose additional ports for testing user applications that send emails. +# smtp_port = 54325 +# pop3_port = 54326 + +[storage] +enabled = true +# The maximum file size allowed (e.g. "5MB", "500KB"). +file_size_limit = "50MiB" + +[auth] +enabled = true +# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used +# in emails. +site_url = "http://127.0.0.1:3000" +# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. +additional_redirect_urls = ["https://127.0.0.1:3000"] +# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). +jwt_expiry = 3600 +# If disabled, the refresh token will never expire. +enable_refresh_token_rotation = true +# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. +# Requires enable_refresh_token_rotation = true. +refresh_token_reuse_interval = 10 +# Allow/disallow new user signups to your project. +enable_signup = true + +[auth.email] +# Allow/disallow new user signups via email to your project. +enable_signup = true +# If enabled, a user will be required to confirm any email change on both the old, and new email +# addresses. If disabled, only the new email is required to confirm. +double_confirm_changes = true +# If enabled, users need to confirm their email address before signing in. +enable_confirmations = false + +# Uncomment to customize email template +# [auth.email.template.invite] +# subject = "You have been invited" +# content_path = "./supabase/templates/invite.html" + +[auth.sms] +# Allow/disallow new user signups via SMS to your project. +enable_signup = true +# If enabled, users need to confirm their phone number before signing in. +enable_confirmations = false +# Template for sending OTP to users +template = "Your code is {{ .Code }} ." + +# Use pre-defined map of phone number to OTP for testing. +[auth.sms.test_otp] +# 4152127777 = "123456" + +# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. +[auth.sms.twilio] +enabled = false +account_sid = "" +message_service_sid = "" +# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: +auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" + +# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, +# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin`, `notion`, `twitch`, +# `twitter`, `slack`, `spotify`, `workos`, `zoom`. +[auth.external.apple] +enabled = false +client_id = "" +# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: +secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" +# Overrides the default auth redirectUrl. +redirect_uri = "" +# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, +# or any other third-party OIDC providers. +url = "" + +[analytics] +enabled = false +port = 54327 +vector_port = 54328 +# Configure one of the supported backends: `postgres`, `bigquery`. +backend = "postgres" + +# Experimental features may be deprecated any time +[experimental] +# Configures Postgres storage engine to use OrioleDB (S3) +orioledb_version = "" +# Configures S3 bucket URL, eg. .s3-accelerate.amazonaws.com +s3_host = "env(S3_HOST)" +# Configures S3 bucket region, eg. us-east-1 +s3_region = "env(S3_REGION)" +# Configures AWS_ACCESS_KEY_ID for S3 bucket +s3_access_key = "env(S3_ACCESS_KEY)" +# Configures AWS_SECRET_ACCESS_KEY for S3 bucket +s3_secret_key = "env(S3_SECRET_KEY)" diff --git a/examples/realtime/flutter-figma-clone/supabase/migrations/20240118161520_canvas.sql b/examples/realtime/flutter-figma-clone/supabase/migrations/20240118161520_canvas.sql new file mode 100644 index 00000000000..5f308c656f2 --- /dev/null +++ b/examples/realtime/flutter-figma-clone/supabase/migrations/20240118161520_canvas.sql @@ -0,0 +1,10 @@ +create table canvas_objects ( + id uuid primary key default gen_random_uuid() not null, + "object" jsonb not null, + created_at timestamp with time zone default timezone('utc'::text, now()) not null +); + +alter table canvas_objects enable row level security; +create policy select_canvas_objects on canvas_objects as permissive for select to anon using (true); +create policy insert_canvas_objects on canvas_objects as permissive for insert to anon with check (true); +create policy update_canvas_objects on canvas_objects as permissive for update to anon using (true); \ No newline at end of file diff --git a/examples/realtime/flutter-figma-clone/supabase/seed.sql b/examples/realtime/flutter-figma-clone/supabase/seed.sql new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/realtime/flutter-figma-clone/web/favicon.png b/examples/realtime/flutter-figma-clone/web/favicon.png new file mode 100644 index 00000000000..8aaa46ac1ae Binary files /dev/null and b/examples/realtime/flutter-figma-clone/web/favicon.png differ diff --git a/examples/realtime/flutter-figma-clone/web/icons/Icon-192.png b/examples/realtime/flutter-figma-clone/web/icons/Icon-192.png new file mode 100644 index 00000000000..b749bfef074 Binary files /dev/null and b/examples/realtime/flutter-figma-clone/web/icons/Icon-192.png differ diff --git a/examples/realtime/flutter-figma-clone/web/icons/Icon-512.png b/examples/realtime/flutter-figma-clone/web/icons/Icon-512.png new file mode 100644 index 00000000000..88cfd48dff1 Binary files /dev/null and b/examples/realtime/flutter-figma-clone/web/icons/Icon-512.png differ diff --git a/examples/realtime/flutter-figma-clone/web/icons/Icon-maskable-192.png b/examples/realtime/flutter-figma-clone/web/icons/Icon-maskable-192.png new file mode 100644 index 00000000000..eb9b4d76e52 Binary files /dev/null and b/examples/realtime/flutter-figma-clone/web/icons/Icon-maskable-192.png differ diff --git a/examples/realtime/flutter-figma-clone/web/icons/Icon-maskable-512.png b/examples/realtime/flutter-figma-clone/web/icons/Icon-maskable-512.png new file mode 100644 index 00000000000..d69c56691fb Binary files /dev/null and b/examples/realtime/flutter-figma-clone/web/icons/Icon-maskable-512.png differ diff --git a/examples/realtime/flutter-figma-clone/web/index.html b/examples/realtime/flutter-figma-clone/web/index.html new file mode 100644 index 00000000000..e0c9b701ea4 --- /dev/null +++ b/examples/realtime/flutter-figma-clone/web/index.html @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + design + + + + + + + + + + diff --git a/examples/realtime/flutter-figma-clone/web/manifest.json b/examples/realtime/flutter-figma-clone/web/manifest.json new file mode 100644 index 00000000000..2c8de133299 --- /dev/null +++ b/examples/realtime/flutter-figma-clone/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "design", + "short_name": "design", + "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" + } + ] +}