mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
examples: Add Flutter Figma clone app to the realtime examples (#20726)
* examples: Add Flutter Figma clone app to the realtime examples * change the folder name to flutter-figma-clone
This commit is contained in:
1 parent
49561167dd
commit
7b26792d3f
22 files changed
+1598
No files matched your search
@@ -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
|
||||
@@ -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'
|
||||
@@ -0,0 +1,19 @@
|
||||
# Flutter 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)
|
||||
@@ -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
|
||||
@@ -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<String, dynamic> json) {
|
||||
final objectType = json['object_type'];
|
||||
if (objectType == UserCursor.type) {
|
||||
return UserCursor.fromJson(json);
|
||||
} else {
|
||||
return CanvasObject.fromJson(json);
|
||||
}
|
||||
}
|
||||
|
||||
SyncedObject({
|
||||
required this.id,
|
||||
});
|
||||
|
||||
Map<String, dynamic> 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<String, dynamic> json)
|
||||
: position = Offset(json['position']['x'], json['position']['y']),
|
||||
color = RandomColor.getRandomFromId(json['id']),
|
||||
super(id: json['id']);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<CanvasPage> createState() => _CanvasPageState();
|
||||
}
|
||||
|
||||
class _CanvasPageState extends State<CanvasPage> {
|
||||
/// Holds the cursor information of other users
|
||||
final Map<String, UserCursor> _userCursors = {};
|
||||
|
||||
/// Holds the list of objects drawn on the canvas
|
||||
final Map<String, CanvasObject> _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<void> _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<void> _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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:canvas/canvas/canvas_object.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CanvasPainter extends CustomPainter {
|
||||
final Map<String, UserCursor> userCursors;
|
||||
final Map<String, CanvasObject> 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;
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
# Supabase
|
||||
.branches
|
||||
.temp
|
||||
.env
|
||||
@@ -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. <bucket>.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)"
|
||||
@@ -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);
|
||||
Whitespace-only changes.
Binary file not shown.
|
After Width: | Height: | Size: 917 B |
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!--
|
||||
If you are serving your web app in a path other than the root, change the
|
||||
href value below to reflect the base path you are serving from.
|
||||
|
||||
The path provided below has to start and end with a slash "/" in order for
|
||||
it to work correctly.
|
||||
|
||||
For more details:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
||||
|
||||
This is a placeholder for base href that will be replaced by the value of
|
||||
the `--base-href` argument provided to `flutter build`.
|
||||
-->
|
||||
<base href="$FLUTTER_BASE_HREF">
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="A new Flutter project.">
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="design">
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
|
||||
<title>design</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
|
||||
<script>
|
||||
// The value below is injected by flutter build, do not touch.
|
||||
const serviceWorkerVersion = null;
|
||||
</script>
|
||||
<!-- This script adds the flutter initialization JS code -->
|
||||
<script src="flutter.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.addEventListener('load', function(ev) {
|
||||
// Download main.dart.js
|
||||
_flutter.loader.loadEntrypoint({
|
||||
serviceWorker: {
|
||||
serviceWorkerVersion: serviceWorkerVersion,
|
||||
},
|
||||
onEntrypointLoaded: function(engineInitializer) {
|
||||
engineInitializer.initializeEngine().then(function(appRunner) {
|
||||
appRunner.runApp();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user