mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
example: Update Flutter Figma clone app (#28593)
Add authorizations to Flutter figma clone
This commit is contained in:
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 @@
|
||||
|
||||

|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user