example: Update Flutter Figma clone app (#28593)

Add authorizations to Flutter figma clone
This commit is contained in:
Tyler authored and GitHub committed 2024-08-13 21:14:16 +09:00
1 parent 5869005d85
commit 76e208371c
18 files changed
+760 -177

No files matched your search

@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
revision: "b0366e0a3f089e15fd89c97604ab402fe26b724c"
revision: "761747bfc538b5af34aa0d3fac380f1bc331ec49"
channel: "stable"
project_type: app
@@ -13,26 +13,11 @@ project_type: app
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
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
- platform: web
create_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c
base_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c
- platform: windows
create_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c
base_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
# User provided section
@@ -2,8 +2,9 @@
![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.
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.
- Authorized Realtime channel
- Cursor position sharing in real-time
- Drawing shapes in real-time
@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:supabase_auth_ui/supabase_auth_ui.dart';
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Login')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 500),
child: SupaEmailAuth(
onSignUpComplete: (AuthResponse response) async {
context.go('/projects');
},
onSignInComplete: (AuthResponse response) {
context.go('/projects');
},
metadataFields: [
MetaDataField(
label: 'Username',
key: 'username',
validator: (val) {
if (val == null) {
return 'Please enter a username';
}
if (RegExp(r'^[A-Za-z0-9_]{3,24}$').hasMatch(val)) {
return null;
} else {
return 'Invalid username';
}
},
),
],
),
),
),
),
);
}
}
@@ -1,12 +1,13 @@
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/models/canvas_object.dart';
import 'package:canvas/models/profile.dart';
import 'package:canvas/models/project.dart';
import 'package:canvas/utils/constants.dart';
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:uuid/uuid.dart';
import 'package:supabase_auth_ui/supabase_auth_ui.dart';
/// Different input modes users can perform
enum _DrawMode {
@@ -27,7 +28,12 @@ enum _DrawMode {
/// Interactive art board page to draw and collaborate with other users.
class CanvasPage extends StatefulWidget {
const CanvasPage({super.key});
const CanvasPage(
this.projectId, {
super.key,
});
final String projectId;
@override
State<CanvasPage> createState() => _CanvasPageState();
@@ -43,8 +49,8 @@ class _CanvasPageState extends State<CanvasPage> {
/// Supabase realtime channel to communicate to other clients
late final RealtimeChannel _canvasChanel;
/// Randomly generated UUID for the user
late final String _myId;
/// Username of each users
String? _myUsername;
/// Whether the user is using the pointer to move things around, or in drawing mode.
_DrawMode _currentMode = _DrawMode.pointer;
@@ -58,21 +64,51 @@ class _CanvasPageState extends State<CanvasPage> {
/// Cursor position of the user.
Offset _cursorPosition = const Offset(0, 0);
Project? _project;
final TextEditingController _addUserController = TextEditingController();
@override
void initState() {
super.initState();
_initialize();
}
@override
void dispose() {
_canvasChanel.unsubscribe();
_addUserController.dispose();
super.dispose();
}
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();
final projectId = widget.projectId;
// Get the project data
_project = await supabase
.from('projects')
.select('*, profiles(*)')
.eq('id', widget.projectId)
.single()
.withConverter(Project.fromJson);
// Get the user's username
final profileMap = await supabase
.from('profiles')
.select('username')
.eq('id', supabase.auth.currentUser!.id)
.maybeSingle();
_myUsername = profileMap?['username'] as String;
// Start listening to broadcast messages to display other users' cursors and objects.
_canvasChanel = supabase
.channel(Constants.channelName)
.channel(
projectId,
opts: const RealtimeChannelConfig(
private: true,
ack: true,
),
)
.onBroadcast(
event: Constants.broadcastEventName,
callback: (payload) {
@@ -85,11 +121,22 @@ class _CanvasPageState extends State<CanvasPage> {
}
setState(() {});
})
.subscribe();
.subscribe((status, error) {
if (status == RealtimeSubscribeStatus.channelError) {
debugPrint('Error subscribing to channel: $error');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Error connecting'),
backgroundColor: Theme.of(context).colorScheme.error,
),
);
}
});
final initialData = await supabase
.from('canvas_objects')
.select()
.eq('project_id', widget.projectId)
.order('created_at', ascending: true);
for (final canvasObjectData in initialData) {
final canvasObject = CanvasObject.fromJson(canvasObjectData['object']);
@@ -100,12 +147,17 @@ class _CanvasPageState extends State<CanvasPage> {
/// Syncs the user's cursor position and the currently drawing object with
/// other users.
Future<void> _syncCanvasObject(Offset cursorPosition) {
///
/// The return value indicates whether the sync was successful or not.
Future<void> _syncCanvasObject(Offset cursorPosition) async {
if (_myUsername == null) {
return;
}
final myCursor = UserCursor(
position: cursorPosition,
id: _myId,
id: _myUsername!,
);
return _canvasChanel.sendBroadcastMessage(
await _canvasChanel.sendBroadcastMessage(
event: Constants.broadcastEventName,
payload: {
'cursor': myCursor.toJson(),
@@ -126,25 +178,25 @@ class _CanvasPageState extends State<CanvasPage> {
// 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)) {
if (canvasObject.intersectsWith(details.localPosition)) {
_currentlyDrawingObjectId = canvasObject.id;
break;
}
}
break;
case _DrawMode.circle:
final newObject = Circle.createNew(details.globalPosition);
final newObject = Circle.createNew(details.localPosition);
_canvasObjects[newObject.id] = newObject;
_currentlyDrawingObjectId = newObject.id;
break;
case _DrawMode.rectangle:
final newObject = Rectangle.createNew(details.globalPosition);
final newObject = Rectangle.createNew(details.localPosition);
_canvasObjects[newObject.id] = newObject;
_currentlyDrawingObjectId = newObject.id;
break;
}
_cursorPosition = details.globalPosition;
_panStartPoint = details.globalPosition;
_cursorPosition = details.localPosition;
_panStartPoint = details.localPosition;
setState(() {});
}
@@ -167,9 +219,9 @@ class _CanvasPageState extends State<CanvasPage> {
_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()) /
center: (details.localPosition + _panStartPoint!) / 2,
radius: min((details.localPosition.dx - _panStartPoint!.dx).abs(),
(details.localPosition.dy - _panStartPoint!.dy).abs()) /
2,
);
break;
@@ -178,7 +230,7 @@ class _CanvasPageState extends State<CanvasPage> {
case _DrawMode.rectangle:
_canvasObjects[_currentlyDrawingObjectId!] =
(_canvasObjects[_currentlyDrawingObjectId!] as Rectangle).copyWith(
bottomRight: details.globalPosition,
bottomRight: details.localPosition,
);
break;
}
@@ -186,17 +238,17 @@ class _CanvasPageState extends State<CanvasPage> {
if (_currentlyDrawingObjectId != null) {
setState(() {});
}
_cursorPosition = details.globalPosition;
_cursorPosition = details.localPosition;
_syncCanvasObject(_cursorPosition);
}
void onPanEnd(DragEndDetails _) async {
if (_currentlyDrawingObjectId != null) {
_syncCanvasObject(_cursorPosition);
}
final drawnObjectId = _currentlyDrawingObjectId;
if (_currentlyDrawingObjectId != null) {
await _syncCanvasObject(_cursorPosition);
}
setState(() {
_panStartPoint = null;
_currentlyDrawingObjectId = null;
@@ -206,8 +258,10 @@ class _CanvasPageState extends State<CanvasPage> {
if (drawnObjectId == null) {
return;
}
await supabase.from('canvas_objects').upsert({
'id': drawnObjectId,
'project_id': widget.projectId,
'object': _canvasObjects[drawnObjectId]!.toJson(),
});
}
@@ -215,46 +269,136 @@ class _CanvasPageState extends State<CanvasPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
titleTextStyle: const TextStyle(color: Colors.white),
title: Text(_project?.name ?? ''),
leadingWidth: 300,
backgroundColor: Colors.grey[900],
leading: Row(
children: _DrawMode.values
.map((mode) => IconButton(
onPressed: () {
setState(() {
_currentMode = mode;
});
},
icon: Icon(mode.iconData),
color: _currentMode == mode ? Colors.green : Colors.white,
))
.toList(),
),
// Displays the list of users currently drawing on the canvas
actions: [
...[
..._userCursors.values.map((e) => e.id),
if (_myUsername != null) _myUsername!
]
.map(
(id) => Align(
widthFactor: 0.8,
child: CircleAvatar(
backgroundColor: RandomColor.getRandomFromId(id),
child: Text(id.substring(0, 2)),
),
),
)
.toList(),
const SizedBox(width: 8),
TextButton.icon(
style: TextButton.styleFrom(foregroundColor: Colors.white),
onPressed: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Share the project'),
content: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
const ListTile(
title: Text('Members'),
),
..._project!.profiles
.map((profile) => ListTile(
title: Text(profile.username),
leading: CircleAvatar(
backgroundColor:
RandomColor.getRandomFromId(
profile.username,
),
child: Text(
profile.username.substring(0, 2)),
),
))
.toList(),
Row(
children: [
Expanded(
child: TextFormField(
controller: _addUserController,
decoration: const InputDecoration(
hintText: 'Add people with username',
),
),
),
TextButton(
onPressed: () async {
final username = _addUserController.text;
final profileMap = await supabase
.from('profiles')
.select()
.eq('username', username)
.maybeSingle();
if (profileMap == null) {
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(
content: Text('User not found')));
return;
}
final profile =
Profile.fromJson(profileMap);
await supabase
.from('project_members')
.insert({
'project_id': widget.projectId,
'profile_id': profile.id,
});
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'${profile.username} has been added')));
},
child: const Text('Invite'),
),
],
)
],
),
);
});
},
label: const Text('Share'),
icon: const Icon(Icons.lock),
),
const SizedBox(width: 20),
]),
body: MouseRegion(
onHover: (event) {
_syncCanvasObject(event.position);
_syncCanvasObject(event.localPosition);
},
child: Stack(
children: [
// The main canvas
GestureDetector(
onPanDown: _onPanDown,
onPanUpdate: _onPanUpdate,
onPanEnd: onPanEnd,
child: CustomPaint(
size: MediaQuery.of(context).size,
painter: CanvasPainter(
userCursors: _userCursors,
canvasObjects: _canvasObjects,
),
),
child: GestureDetector(
onPanDown: _onPanDown,
onPanUpdate: _onPanUpdate,
onPanEnd: onPanEnd,
child: CustomPaint(
size: MediaQuery.of(context).size,
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(),
),
),
],
),
),
),
);
@@ -1,4 +1,4 @@
import 'package:canvas/canvas/canvas_object.dart';
import 'package:canvas/models/canvas_object.dart';
import 'package:flutter/material.dart';
class CanvasPainter extends CustomPainter {
@@ -1,27 +1,64 @@
import 'package:canvas/auth/login_page.dart';
import 'package:canvas/canvas/canvas_page.dart';
import 'package:canvas/project/projects_page.dart';
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:flutter_web_plugins/url_strategy.dart';
import 'package:go_router/go_router.dart';
import 'package:supabase_auth_ui/supabase_auth_ui.dart';
void main() async {
Supabase.initialize(
url: 'http://127.0.0.1:54321',
anonKey:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0',
usePathUrlStrategy();
await Supabase.initialize(
url: 'YOUR_SUPABASE_URL',
anonKey: 'YOUR_SUPABASE_ANON_KEY',
);
runApp(const MyApp());
}
final supabase = Supabase.instance.client;
final _router = GoRouter(
initialLocation: '/signin',
routes: [
GoRoute(
path: '/signin',
builder: (context, state) => const LoginPage(),
),
GoRoute(
path: '/projects',
builder: (context, state) => const ProjectsPage(),
),
GoRoute(
path: '/canvas/:projectId',
builder: (context, state) =>
CanvasPage(state.pathParameters['projectId']!),
),
],
redirect: (BuildContext context, GoRouterState state) {
final isSignedIn = supabase.auth.currentSession != null;
if (state.path != '/signin') {
if (!isSignedIn) {
return '/signin';
}
} else {
if (isSignedIn) {
return '/projects';
}
}
return null;
},
);
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
return MaterialApp.router(
title: 'Figma Clone',
debugShowCheckedModeBanner: false,
home: CanvasPage(),
routerConfig: _router,
);
}
}
@@ -0,0 +1,17 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
class Profile {
final String id;
final String username;
Map<String, dynamic> toJson() {
return <String, dynamic>{
'id': id,
'username': username,
};
}
Profile.fromJson(Map<String, dynamic> map)
: id = map['id'] as String,
username = map['username'] as String;
}
@@ -0,0 +1,25 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:canvas/models/profile.dart';
class Project {
final String id;
final String name;
final List<Profile> profiles;
Map<String, dynamic> toJson() {
return <String, dynamic>{
'id': id,
'name': name,
'profiles': profiles.map((profile) => profile.toJson()).toList(),
};
}
Project.fromJson(Map<String, dynamic> map)
: id = map['id'] as String,
name = map['name'] as String,
profiles = List<Profile>.from(
(map['profiles'] as List<dynamic>).map<Profile>(
(profile) => Profile.fromJson(profile as Map<String, dynamic>),
),
);
}
@@ -0,0 +1,110 @@
import 'package:canvas/main.dart';
import 'package:canvas/models/canvas_object.dart';
import 'package:canvas/models/project.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class ProjectsPage extends StatelessWidget {
const ProjectsPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
extendBody: true,
appBar: AppBar(title: const Text('Projects')),
body: FutureBuilder<List<Project>>(
future: supabase
.from('projects')
.select('*, profiles(*)')
.order('created_at')
.withConverter((rows) => rows.map(Project.fromJson).toList()),
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Center(child: Text('An error occurred'));
}
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final projects = snapshot.data!;
if (projects.isEmpty) {
return const Center(
child: Text('You do not have any projects yet'),
);
}
return Align(
alignment: Alignment.topCenter,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Wrap(
runAlignment: WrapAlignment.spaceAround,
spacing: 16,
runSpacing: 16,
children: projects.map(
(project) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 300),
child: Material(
elevation: 4,
borderRadius: BorderRadius.circular(8),
color: Colors.white,
child: InkWell(
onTap: () {
context.go('/canvas/${project.id}');
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
project.name,
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: project.profiles
.map(
(profile) => Align(
widthFactor: 0.8,
child: CircleAvatar(
backgroundColor:
RandomColor.getRandomFromId(
profile.username,
),
child: Text(profile.username
.substring(0, 2)),
),
),
)
.toList(),
),
],
),
),
),
),
);
},
).toList(),
),
),
);
}),
floatingActionButton: ElevatedButton.icon(
onPressed: () async {
final projectId = await supabase.rpc('create_new_project');
context.go('/canvas/$projectId');
},
label: const Text('Create a Project'),
icon: const Icon(Icons.add),
),
);
}
}
@@ -1,7 +1,4 @@
abstract class Constants {
/// Name of the realtime channel
static const String channelName = 'canvas';
/// Name of the broadcast event
static const String broadcastEventName = 'canvas';
}
@@ -57,6 +57,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.3"
email_validator:
dependency: transitive
description:
name: email_validator
sha256: e9a90f27ab2b915a27d7f9c2a7ddda5dd752d6942616ee83529b686fc086221b
url: "https://pub.dev"
source: hosted
version: "2.1.17"
fake_async:
dependency: transitive
description:
@@ -112,22 +120,86 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
font_awesome_flutter:
dependency: transitive
description:
name: font_awesome_flutter
sha256: "275ff26905134bcb59417cf60ad979136f1f8257f2f449914b2c3e05bbb4cd6f"
url: "https://pub.dev"
source: hosted
version: "10.7.0"
functions_client:
dependency: transitive
description:
name: functions_client
sha256: "9a0ab83a525c8691a6724746e642de755a299afa04158807787364cd9e718001"
sha256: "229648c4c78e0cb13dfb5e7508a3d2e7058f126ba364b94f8199ba4cead96d4e"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
version: "2.3.1"
go_router:
dependency: "direct main"
description:
name: go_router
sha256: ddc16d34b0d74cb313986918c0f0885a7ba2fc24d8fb8419de75f0015144ccfe
url: "https://pub.dev"
source: hosted
version: "14.2.3"
google_identity_services_web:
dependency: transitive
description:
name: google_identity_services_web
sha256: "0c56c2c5d60d6dfaf9725f5ad4699f04749fb196ee5a70487a46ef184837ccf6"
url: "https://pub.dev"
source: hosted
version: "0.3.0+2"
google_sign_in:
dependency: transitive
description:
name: google_sign_in
sha256: "0b8787cb9c1a68ad398e8010e8c8766bfa33556d2ab97c439fb4137756d7308f"
url: "https://pub.dev"
source: hosted
version: "6.2.1"
google_sign_in_android:
dependency: transitive
description:
name: google_sign_in_android
sha256: "5a47ebec9af97daf0822e800e4f101c3340b5ebc3f6898cf860c1a71b53cf077"
url: "https://pub.dev"
source: hosted
version: "6.1.28"
google_sign_in_ios:
dependency: transitive
description:
name: google_sign_in_ios
sha256: a058c9880be456f21e2e8571c1126eaacd570bdc5b6c6d9d15aea4bdf22ca9fe
url: "https://pub.dev"
source: hosted
version: "5.7.6"
google_sign_in_platform_interface:
dependency: transitive
description:
name: google_sign_in_platform_interface
sha256: "1f6e5787d7a120cc0359ddf315c92309069171306242e181c09472d1b00a2971"
url: "https://pub.dev"
source: hosted
version: "2.4.5"
google_sign_in_web:
dependency: transitive
description:
name: google_sign_in_web
sha256: f2b3af0ba52ff59439f18962fca71db860f09507a81da929fc0e719270b35db2
url: "https://pub.dev"
source: hosted
version: "0.12.3+3"
gotrue:
dependency: transitive
description:
name: gotrue
sha256: b6c30fa49dfb243f9c61214651ffe2611704b46fddb520125b5f45610cb9a118
sha256: a8784341bcc08f88ba7a4b04a40a37059c7e71c315f058d45c31d09e8a951194
url: "https://pub.dev"
source: hosted
version: "2.4.0"
version: "2.8.3"
gtk:
dependency: transitive
description:
@@ -136,22 +208,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
hive:
dependency: transitive
description:
name: hive
sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941"
url: "https://pub.dev"
source: hosted
version: "2.2.3"
hive_flutter:
dependency: transitive
description:
name: hive_flutter
sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc
url: "https://pub.dev"
source: hosted
version: "1.1.0"
http:
dependency: transitive
description:
@@ -176,6 +232,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.3.1"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
url: "https://pub.dev"
source: hosted
version: "10.0.5"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
url: "https://pub.dev"
source: hosted
version: "3.0.5"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
lints:
dependency: transitive
description:
@@ -184,30 +264,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.1"
logging:
dependency: transitive
description:
name: logging
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
source: hosted
version: "0.12.16"
version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.5.0"
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
url: "https://pub.dev"
source: hosted
version: "1.10.0"
version: "1.15.0"
mime:
dependency: transitive
description:
@@ -220,10 +308,10 @@ packages:
dependency: transitive
description:
name: path
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
source: hosted
version: "1.8.3"
version: "1.9.0"
path_provider:
dependency: transitive
description:
@@ -292,18 +380,18 @@ packages:
dependency: transitive
description:
name: postgrest
sha256: "748ebffffb60b4eaa270955dcf3742a19a2b315344c41ff1b4a0ebcd322b5181"
sha256: "675540a5e93e89fc28cc4ef0999ed2916c00a1c73de73f3a0d5067ce36dd41a2"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.1.3"
realtime_client:
dependency: transitive
description:
name: realtime_client
sha256: "5831636c19802ba936093a35a7c5b745b130e268fa052e84b4b5290139d2ae03"
sha256: a99b7817e203c57ada746e9fe113820410cf84d9029f4310c57737aae890b0f7
url: "https://pub.dev"
source: hosted
version: "2.0.0"
version: "2.2.0"
retry:
dependency: transitive
description:
@@ -376,6 +464,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.2"
sign_in_with_apple:
dependency: transitive
description:
name: sign_in_with_apple
sha256: "5c34c601d6910d7db0077063001ccd9d9329ee58b9038ee07153ffcdd76e91bd"
url: "https://pub.dev"
source: hosted
version: "6.1.1"
sign_in_with_apple_platform_interface:
dependency: transitive
description:
name: sign_in_with_apple_platform_interface
sha256: c2ef2ce6273fce0c61acd7e9ff5be7181e33d7aa2b66508b39418b786cca2119
url: "https://pub.dev"
source: hosted
version: "1.1.0"
sign_in_with_apple_web:
dependency: transitive
description:
name: sign_in_with_apple_web
sha256: c009e9beeb6c376e86aaa154fcc8b4e075d4bad90c56286b9668a51cdb6129ea
url: "https://pub.dev"
source: hosted
version: "2.1.0"
sky_engine:
dependency: transitive
description: flutter
@@ -409,10 +521,10 @@ packages:
dependency: transitive
description:
name: storage_client
sha256: b49ff2e1e6738c0ef445546d6ec77040829947f0c7ef0b115acb125656127c83
sha256: e37f1b9d40f43078d12bd2d1b6b08c2c16fbdbafc58b57bc44922da6ea3f5625
url: "https://pub.dev"
source: hosted
version: "2.0.0"
version: "2.0.2"
stream_channel:
dependency: transitive
description:
@@ -433,18 +545,26 @@ packages:
dependency: transitive
description:
name: supabase
sha256: "976baea81ed75e3b5925db13fe0bfed79440e3b2d8d0753e2f58ab402457b467"
sha256: f6d21d0b555aa997d8170afd112407df0aaf44820cbe93b3520e2f7ea3ec0904
url: "https://pub.dev"
source: hosted
version: "2.0.5"
supabase_flutter:
version: "2.2.7"
supabase_auth_ui:
dependency: "direct main"
description:
name: supabase_flutter
sha256: "4bf88de6ff3c7c350863c907f02f02db5f5a112a826c5a6c02de69d52898bf30"
name: supabase_auth_ui
sha256: "2a60c9e513360f062e4aa016760a48a5585c0986f1ff178e22f1fa88534e2f6b"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
version: "0.5.0"
supabase_flutter:
dependency: transitive
description:
name: supabase_flutter
sha256: bcac7745e423ffe4aea86fefc3ccef259f979005c7a40558089c11f8606fd085
url: "https://pub.dev"
source: hosted
version: "2.5.11"
term_glyph:
dependency: transitive
description:
@@ -457,10 +577,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
url: "https://pub.dev"
source: hosted
version: "0.6.1"
version: "0.7.2"
typed_data:
dependency: transitive
description:
@@ -549,14 +669,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc
url: "https://pub.dev"
source: hosted
version: "14.2.4"
web:
dependency: transitive
description:
name: web
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05"
url: "https://pub.dev"
source: hosted
version: "0.3.0"
version: "0.4.2"
web_socket_channel:
dependency: transitive
description:
@@ -585,10 +713,10 @@ packages:
dependency: transitive
description:
name: yet_another_json_isolate
sha256: e727502a2640d65b4b8a8a6cb48af9dd0cbe644ba4b3ee667c7f4afa0c1d6069
sha256: "381dd593e4199fe41a7b6467cef77a41d91fa8ef41465e135f12c99a76be463b"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
version: "2.0.1"
sdks:
dart: ">=3.2.3 <4.0.0"
flutter: ">=3.16.0"
dart: ">=3.4.0 <4.0.0"
flutter: ">=3.22.0"
@@ -12,8 +12,9 @@ dependencies:
flutter:
sdk: flutter
supabase_flutter: ^2.3.0
supabase_auth_ui: ^0.5.0
uuid: ^4.3.3
go_router: ^14.2.3
dev_dependencies:
flutter_test:
@@ -4,7 +4,3 @@ create table canvas_objects (
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);
@@ -0,0 +1,87 @@
create table public.profiles (
id uuid primary key references auth.users(id) on delete cascade,
username text unique not null,
created_at timestamp with time zone default timezone('utc'::text, now()) not null
-- username should be 3 to 24 characters long containing alphabets, numbers and underscores
constraint username_validation check (username ~* '^[A-Za-z0-9_]{3,24}$')
);
-- Function to create a new row in profiles table upon signup
-- Also copies the username value from metadata
create or replace function handle_new_user() returns trigger as $$
begin
insert into public.profiles(id, username)
values(new.id, new.raw_user_meta_data->>'username');
return new;
end;
$$ language plpgsql security definer;
-- Trigger to call `handle_new_user` when new user signs up
create trigger on_auth_user_created
after insert on auth.users
for each row
execute function handle_new_user();
create table if not exists public.projects (
id uuid primary key default gen_random_uuid(),
name text not null default 'new project',
created_at timestamp with time zone default timezone('utc'::text, now()) not null
);
create table if not exists public.project_members (
profile_id uuid references public.profiles(id) on delete cascade not null,
project_id uuid references public.projects(id) on delete cascade not null,
created_at timestamp with time zone default timezone('utc' :: text, now()) not null,
primary key (profile_id, project_id)
);
alter table public.canvas_objects
add column project_id uuid references public.projects(id) on delete cascade not null;
-- Returns true if the signed in user is a member of the project
create or replace function is_project_member(project_id uuid)
returns boolean as $$
select exists(
select 1
from project_members
where project_id = is_project_member.project_id and profile_id = auth.uid()
);
$$ language sql security definer;
alter table public.profiles enable row level security;
create policy "Public profiles are viewable by everyone." on public.profiles for select using (true);
alter table public.projects enable row level security;
create policy "Users can view projects that they have joined" on public.projects for select using (is_project_member(id));
alter table public.project_members enable row level security;
create policy "Participants of the project can view other participants." on public.project_members for select using (is_project_member(project_id));
create policy "Participants of the project can insert new users." on public.project_members for insert with check (is_project_member(project_id));
alter table public.canvas_objects enable row level security;
create policy "Users can view canvas_objects of projects they are in." on public.canvas_objects for select using (is_project_member(project_id));
create policy "Users can insert canvas_objects of projects they are in." on public.canvas_objects for insert with check (is_project_member(project_id));
create policy "Users can update canvas_objects of projects they are in." on public.canvas_objects for update using (is_project_member(project_id)) with check (is_project_member(project_id));
create policy "Users can delete canvas_objects of projects they are in." on public.canvas_objects for delete using (is_project_member(project_id));
-- Creates a new project and inserts the caller as a participant
create or replace function create_new_project() returns uuid as $$
declare
new_project_id uuid;
begin
-- Create a new project
insert into public.projects default values
returning id into new_project_id;
-- Insert the caller user into the new project
insert into public.project_members (profile_id, project_id)
values (auth.uid(), new_project_id);
return new_project_id;
end
$$ language plpgsql security definer;
create policy "Project members can receive presence and broadcast messages." on realtime.messages for select using (is_project_member(realtime.topic()::uuid));
create policy "Project members can send presence and broadcast messages." on realtime.messages for insert with check (is_project_member(realtime.topic()::uuid));
@@ -0,0 +1,30 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:canvas/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}
@@ -23,37 +23,16 @@
<!-- 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">
<meta name="apple-mobile-web-app-title" content="canvas">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>design</title>
<title>canvas</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>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>
@@ -1,6 +1,6 @@
{
"name": "design",
"short_name": "design",
"name": "canvas",
"short_name": "canvas",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",