Compare commits

...

2 Commits

Author SHA1 Message Date
bad485ac1d go based routing for groups 2024-01-14 15:50:38 +00:00
bfd828f520 more robust loading of projects and groups 2024-01-14 13:40:49 +00:00
12 changed files with 153 additions and 126 deletions

View File

@ -29,6 +29,8 @@ def get(user, id):
owner = user and (user.get('_id') == proj['user']) owner = user and (user.get('_id') == proj['user'])
if not owner and proj['visibility'] != 'public': if not owner and proj['visibility'] != 'public':
raise util.errors.BadRequest('Forbidden') raise util.errors.BadRequest('Forbidden')
if obj['type'] == 'file' and 'storedName' in obj:
obj['url'] = uploads.get_presigned_url('projects/{0}/{1}'.format(proj['_id'], obj['storedName']))
if obj['type'] == 'pattern' and 'preview' in obj and '.png' in obj['preview']: if obj['type'] == 'pattern' and 'preview' in obj and '.png' in obj['preview']:
obj['previewUrl'] = uploads.get_presigned_url('projects/{0}/{1}'.format(proj['_id'], obj['preview'])) obj['previewUrl'] = uploads.get_presigned_url('projects/{0}/{1}'.format(proj['_id'], obj['preview']))
del obj['preview'] del obj['preview']

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -6,31 +6,41 @@ import 'group_noticeboard.dart';
import 'group_members.dart'; import 'group_members.dart';
class _GroupScreenState extends State<GroupScreen> { class _GroupScreenState extends State<GroupScreen> {
final String id;
Map<String, dynamic>? _group;
int _selectedIndex = 0; int _selectedIndex = 0;
List<Widget> _widgetOptions = <Widget> [];
final Map<String, dynamic> _group;
_GroupScreenState(this._group) { _GroupScreenState(this.id) { }
_widgetOptions = <Widget> [
GroupNoticeBoardTab(this._group), @override
GroupMembersTab(this._group) void initState() {
]; fetchGroup();
super.initState();
} }
void _onItemTapped(int index) { void fetchGroup() async {
Api api = Api();
var data = await api.request('GET', '/groups/' + id);
if (data['success'] == true) {
setState(() { setState(() {
_selectedIndex = index; _group = data['payload'];
}); });
} }
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(_group['name']) title: Text(_group?['name'] ?? 'Group')
), ),
body: Center( body: Center(
child: _widgetOptions.elementAt(_selectedIndex), child: _group != null ?
[
GroupNoticeBoardTab(_group!),
GroupMembersTab(_group!)
].elementAt(_selectedIndex)
: CircularProgressIndicator(),
), ),
bottomNavigationBar: BottomNavigationBar( bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[ items: const <BottomNavigationBarItem>[
@ -45,15 +55,17 @@ class _GroupScreenState extends State<GroupScreen> {
], ],
currentIndex: _selectedIndex, currentIndex: _selectedIndex,
selectedItemColor: Colors.pink[600], selectedItemColor: Colors.pink[600],
onTap: _onItemTapped, onTap: (int index) => setState(() {
_selectedIndex = index;
}),
), ),
); );
} }
} }
class GroupScreen extends StatefulWidget { class GroupScreen extends StatefulWidget {
final Map<String,dynamic> group; final String id;
GroupScreen(this.group) { } GroupScreen(this.id) { }
@override @override
_GroupScreenState createState() => _GroupScreenState(group); _GroupScreenState createState() => _GroupScreenState(id);
} }

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'group.dart'; import 'package:go_router/go_router.dart';
import 'api.dart'; import 'api.dart';
import 'model.dart'; import 'model.dart';
import 'lib.dart'; import 'lib.dart';
@ -16,6 +16,8 @@ class _GroupsTabState extends State<GroupsTab> {
} }
void getGroups() async { void getGroups() async {
AppModel model = Provider.of<AppModel>(context, listen: false);
if (model.user == null) return;
setState(() => _loading = true); setState(() => _loading = true);
Api api = Api(); Api api = Api();
var data = await api.request('GET', '/groups'); var data = await api.request('GET', '/groups');
@ -36,14 +38,7 @@ class _GroupsTabState extends State<GroupsTab> {
} }
return Card( return Card(
child: InkWell( child: InkWell(
onTap: () { onTap: () => context.push('/groups/' + group['_id']),
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => GroupScreen(group),
),
);
},
child: ListTile( child: ListTile(
leading: Icon(Icons.people), leading: Icon(Icons.people),
trailing: Icon(Icons.keyboard_arrow_right), trailing: Icon(Icons.keyboard_arrow_right),

View File

@ -263,12 +263,7 @@ class PatternCard extends StatelessWidget {
), ),
child: InkWell( child: InkWell(
onTap: () { onTap: () {
Navigator.push( context.push('/' + object['userObject']['username'] + '/' + object['projectObject']['path'] + '/' + object['_id']);
context,
MaterialPageRoute(
builder: (context) => ObjectScreen(object, object['projectObject']),
),
);
}, },
child: Column( child: Column(
children: [ children: [

View File

@ -13,7 +13,9 @@ import 'register.dart';
import 'onboarding.dart'; import 'onboarding.dart';
import 'home.dart'; import 'home.dart';
import 'project.dart'; import 'project.dart';
import 'object.dart';
import 'settings.dart'; import 'settings.dart';
import 'group.dart';
final router = GoRouter( final router = GoRouter(
routes: [ routes: [
@ -37,7 +39,9 @@ final router = GoRouter(
GoRoute(path: '/onboarding', builder: (context, state) => OnboardingScreen()), GoRoute(path: '/onboarding', builder: (context, state) => OnboardingScreen()),
GoRoute(path: '/home', builder: (context, state) => HomeScreen()), GoRoute(path: '/home', builder: (context, state) => HomeScreen()),
GoRoute(path: '/settings', builder: (context, state) => SettingsScreen()), GoRoute(path: '/settings', builder: (context, state) => SettingsScreen()),
GoRoute(path: '/:username/:id', builder: (context, state) => ProjectScreen(state.pathParameters['username']!, state.pathParameters['id']!)), GoRoute(path: '/groups/:id', builder: (context, state) => GroupScreen(state.pathParameters['id']!)),
GoRoute(path: '/:username/:path', builder: (context, state) => ProjectScreen(state.pathParameters['username']!, state.pathParameters['path']!)),
GoRoute(path: '/:username/:path/:id', builder: (context, state) => ObjectScreen(state.pathParameters['username']!, state.pathParameters['path']!, state.pathParameters['id']!)),
], ],
); );
@ -114,9 +118,7 @@ class Startup extends StatelessWidget {
_handled = true; _handled = true;
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
String? token = prefs.getString('apiToken'); String? token = prefs.getString('apiToken');
print('Nooo');
if (token != null) { if (token != null) {
print('HEE');
AppModel model = Provider.of<AppModel>(context, listen: false); AppModel model = Provider.of<AppModel>(context, listen: false);
await model.setToken(token!); await model.setToken(token!);
FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance; FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;

View File

@ -3,6 +3,7 @@ import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/flutter_html.dart';
import 'package:go_router/go_router.dart';
import 'dart:io'; import 'dart:io';
import 'api.dart'; import 'api.dart';
import 'util.dart'; import 'util.dart';
@ -10,30 +11,32 @@ import 'patterns/pattern.dart';
import 'patterns/viewer.dart'; import 'patterns/viewer.dart';
class _ObjectScreenState extends State<ObjectScreen> { class _ObjectScreenState extends State<ObjectScreen> {
final Map<String,dynamic> _project; final String username;
Map<String,dynamic> _object; final String projectPath;
Map<String,dynamic>? _pattern; final String id;
final Map<String,dynamic>? project;
Map<String,dynamic>? object;
Map<String,dynamic>? pattern;
bool _isLoading = false; bool _isLoading = false;
final Function? onUpdate; final Function? onUpdate;
final Function? onDelete; final Function? onDelete;
final Api api = Api(); final Api api = Api();
final Util util = Util(); final Util util = Util();
_ObjectScreenState(this._object, this._project, {this.onUpdate, this.onDelete}) { } _ObjectScreenState(this.username, this.projectPath, this.id, {this.object, this.project, this.onUpdate, this.onDelete}) { }
@override @override
initState() { initState() {
super.initState(); super.initState();
if (_object['type'] == 'pattern') { fetchObject();
_fetchPattern();
}
} }
void _fetchPattern() async { void fetchObject() async {
var data = await api.request('GET', '/objects/' + _object['_id']); var data = await api.request('GET', '/objects/' + id);
if (data['success'] == true) { if (data['success'] == true) {
setState(() { setState(() {
_pattern = data['payload']['pattern']; object = data['payload'];
pattern = data['payload']['pattern'];
}); });
} }
} }
@ -41,14 +44,14 @@ class _ObjectScreenState extends State<ObjectScreen> {
void _shareObject() async { void _shareObject() async {
setState(() => _isLoading = true); setState(() => _isLoading = true);
File? file; File? file;
if (_object['type'] == 'pattern') { if (object!['type'] == 'pattern') {
var data = await api.request('GET', '/objects/' + _object['_id'] + '/wif'); var data = await api.request('GET', '/objects/' + id + '/wif');
if (data['success'] == true) { if (data['success'] == true) {
file = await util.writeFile(_object['name'] + '.wif', data['payload']['wif']); file = await util.writeFile(object!['name'] + '.wif', data['payload']['wif']);
} }
} else { } else {
String fileName = Uri.file(_object['url']).pathSegments.last; String fileName = Uri.file(object!['url']).pathSegments.last;
file = await api.downloadFile(_object['url'], fileName); file = await api.downloadFile(object!['url'], fileName);
} }
if (file != null) { if (file != null) {
@ -58,12 +61,10 @@ class _ObjectScreenState extends State<ObjectScreen> {
} }
void _deleteObject(BuildContext context, BuildContext modalContext) async { void _deleteObject(BuildContext context, BuildContext modalContext) async {
var data = await api.request('DELETE', '/objects/' + _object['_id']); var data = await api.request('DELETE', '/objects/' + id);
if (data['success']) { if (data['success']) {
Navigator.pop(context); context.go('/home');
Navigator.pop(modalContext); onDelete!(id);
Navigator.pop(context);
onDelete!(_object['_id']);
} }
} }
@ -77,7 +78,7 @@ class _ObjectScreenState extends State<ObjectScreen> {
CupertinoDialogAction( CupertinoDialogAction(
isDefaultAction: true, isDefaultAction: true,
child: Text('No'), child: Text('No'),
onPressed: () => Navigator.pop(context), onPressed: () => context.pop(),
), ),
CupertinoDialogAction( CupertinoDialogAction(
isDestructiveAction: true, isDestructiveAction: true,
@ -105,22 +106,22 @@ class _ObjectScreenState extends State<ObjectScreen> {
TextButton( TextButton(
child: Text('CANCEL'), child: Text('CANCEL'),
onPressed: () { onPressed: () {
Navigator.pop(context); context.pop();
}, },
), ),
TextButton( TextButton(
child: Text('OK'), child: Text('OK'),
onPressed: () async { onPressed: () async {
var data = await api.request('PUT', '/objects/' + _object['_id'], {'name': renameController.text}); var data = await api.request('PUT', '/objects/' + id, {'name': renameController.text});
if (data['success']) { if (data['success']) {
Navigator.pop(context); context.pop();
_object['name'] = data['payload']['name']; object!['name'] = data['payload']['name'];
onUpdate!(_object['_id'], data['payload']); onUpdate!(id, data['payload']);
setState(() { setState(() {
_object = _object; object = object;
}); });
} }
Navigator.pop(context); context.pop();
}, },
), ),
], ],
@ -136,7 +137,7 @@ class _ObjectScreenState extends State<ObjectScreen> {
return CupertinoActionSheet( return CupertinoActionSheet(
title: Text('Manage this object'), title: Text('Manage this object'),
cancelButton: CupertinoActionSheetAction( cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.of(modalContext).pop(), onPressed: () => modalContext.pop(),
child: Text('Cancel') child: Text('Cancel')
), ),
actions: [ actions: [
@ -156,15 +157,22 @@ class _ObjectScreenState extends State<ObjectScreen> {
} }
Widget getObjectWidget() { Widget getObjectWidget() {
if (_object['isImage'] == true) { if (object == null) {
return Image.network(_object['url']); return Center(child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [CircularProgressIndicator()]
));
} }
else if (_object['type'] == 'pattern') { else if (object!['isImage'] == true && object!['url'] != null) {
if (_pattern != null) { print(object!['url']);
return PatternViewer(_pattern!, withEditor: true); return Image.network(object!['url']);
} }
else if (_object['previewUrl'] != null) { else if (object!['type'] == 'pattern') {
return Image.network(_object['previewUrl']!);; if (pattern != null) {
return PatternViewer(pattern!, withEditor: true);
}
else if (object!['previewUrl'] != null) {
return Image.network(object!['previewUrl']!);;
} }
else { else {
return Column( return Column(
@ -184,7 +192,7 @@ class _ObjectScreenState extends State<ObjectScreen> {
Text('Treadl cannot display this type of item.'), Text('Treadl cannot display this type of item.'),
SizedBox(height: 20), SizedBox(height: 20),
ElevatedButton(child: Text('View file'), onPressed: () { ElevatedButton(child: Text('View file'), onPressed: () {
launch(_object['url']); launch(object!['url']);
}), }),
], ],
)); ));
@ -194,11 +202,11 @@ class _ObjectScreenState extends State<ObjectScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String description = ''; String description = '';
if (_object['description'] != null) if (object?['description'] != null)
description = _object['description']; description = object!['description']!;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(_object['name']), title: Text(object?['name'] ?? 'Object'),
actions: <Widget>[ actions: <Widget>[
IconButton( IconButton(
icon: Icon(Icons.ios_share), icon: Icon(Icons.ios_share),
@ -228,12 +236,15 @@ class _ObjectScreenState extends State<ObjectScreen> {
} }
class ObjectScreen extends StatefulWidget { class ObjectScreen extends StatefulWidget {
final Map<String,dynamic> _object; final String username;
final Map<String,dynamic> _project; final String projectPath;
final String id;
final Map<String,dynamic>? object;
final Map<String,dynamic>? project;
final Function? onUpdate; final Function? onUpdate;
final Function? onDelete; final Function? onDelete;
ObjectScreen(this._object, this._project, {this.onUpdate, this.onDelete}) { } ObjectScreen(this.username, this.projectPath, this.id, {this.object, this.project, this.onUpdate, this.onDelete}) { }
@override @override
_ObjectScreenState createState() => _ObjectScreenState(_object, _project, onUpdate: onUpdate, onDelete: onDelete); _ObjectScreenState createState() => _ObjectScreenState(username, projectPath, id, object: object, project: project, onUpdate: onUpdate, onDelete: onDelete);
} }

View File

@ -112,7 +112,7 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
CupertinoButton( CupertinoButton(
color: Colors.white, color: Colors.white,
child: Text('Get started', style: TextStyle(color: Colors.pink)), child: Text('Get started', style: TextStyle(color: Colors.pink)),
onPressed: () => context.go('/'), onPressed: () => context.go('/home'),
), ),
] ]
) )

View File

@ -9,7 +9,7 @@ import 'package:intl/intl.dart';
import 'dart:io'; import 'dart:io';
import 'api.dart'; import 'api.dart';
import 'util.dart'; import 'util.dart';
import 'object.dart'; import 'model.dart';
class _ProjectScreenState extends State<ProjectScreen> { class _ProjectScreenState extends State<ProjectScreen> {
final String username; final String username;
@ -252,12 +252,7 @@ class _ProjectScreenState extends State<ProjectScreen> {
return new Card( return new Card(
child: InkWell( child: InkWell(
onTap: () { onTap: () {
Navigator.push( context.push('/' + username + '/' + projectPath + '/' + object['_id']);
context,
MaterialPageRoute(
builder: (context) => ObjectScreen(object, project!, onUpdate: _onUpdateObject, onDelete: _onDeleteObject),
),
);
}, },
child: ListTile( child: ListTile(
leading: leader, leading: leader,
@ -269,8 +264,31 @@ class _ProjectScreenState extends State<ProjectScreen> {
); );
} }
Widget getBody() {
if (_loading || project == null)
return CircularProgressIndicator();
else if ((_objects != null && _objects.length > 0) || _creating)
return ListView.builder(
itemCount: _objects.length + (_creating ? 1 : 0),
itemBuilder: (BuildContext context, int index) {
return getObjectCard(index);
},
);
else
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('This project is currently empty', style: TextStyle(fontSize: 20), textAlign: TextAlign.center),
Image(image: AssetImage('assets/empty.png'), width: 300),
Text('Add a pattern file, an image, or something else to this project using the + button below.', textAlign: TextAlign.center),
]);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
AppModel model = Provider.of<AppModel>(context);
User? user = model.user;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(project?['name'] ?? 'Project'), title: Text(project?['name'] ?? 'Project'),
@ -289,33 +307,13 @@ class _ProjectScreenState extends State<ProjectScreen> {
) : SizedBox(width: 0), ) : SizedBox(width: 0),
] ]
), ),
body: _loading ? body: Container(
Container(
margin: const EdgeInsets.all(10.0), margin: const EdgeInsets.all(10.0),
alignment: Alignment.center, alignment: Alignment.center,
child: CircularProgressIndicator() child: getBody(),
)
: Container(
margin: const EdgeInsets.all(10.0),
child: ((_objects != null && _objects.length > 0) || _creating) ?
ListView.builder(
itemCount: _objects.length + (_creating ? 1 : 0),
itemBuilder: (BuildContext context, int index) {
return getObjectCard(index);
},
)
:
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('This project is currently empty', style: TextStyle(fontSize: 20), textAlign: TextAlign.center),
Image(image: AssetImage('assets/empty.png'), width: 300),
Text('Add a pattern file, an image, or something else to this project using the + button below.', textAlign: TextAlign.center),
])
), ),
floatingActionButtonLocation: ExpandableFab.location, floatingActionButtonLocation: ExpandableFab.location,
floatingActionButton: ExpandableFab( floatingActionButton: user != null ? ExpandableFab(
distance: 70, distance: 70,
type: ExpandableFabType.up, type: ExpandableFabType.up,
openButtonBuilder: RotateFloatingActionButtonBuilder( openButtonBuilder: RotateFloatingActionButtonBuilder(
@ -355,7 +353,7 @@ class _ProjectScreenState extends State<ProjectScreen> {
), ),
]), ]),
], ],
), ) : null,
); );
} }
} }

View File

@ -19,6 +19,8 @@ class _ProjectsTabState extends State<ProjectsTab> {
} }
void getProjects() async { void getProjects() async {
AppModel model = Provider.of<AppModel>(context, listen: false);
if (model.user == null) return;
setState(() { setState(() {
_loading = true; _loading = true;
}); });
@ -131,6 +133,8 @@ class _ProjectsTabState extends State<ProjectsTab> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
AppModel model = Provider.of<AppModel>(context);
User? user = model.user;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('My Projects'), title: Text('My Projects'),
@ -148,11 +152,11 @@ class _ProjectsTabState extends State<ProjectsTab> {
alignment: Alignment.center, alignment: Alignment.center,
child: getBody() child: getBody()
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: user != null ? FloatingActionButton(
onPressed: showNewProjectDialog, onPressed: showNewProjectDialog,
child: _creatingProject ? CircularProgressIndicator(backgroundColor: Colors.white) : Icon(Icons.add), child: _creatingProject ? CircularProgressIndicator(backgroundColor: Colors.white) : Icon(Icons.add),
backgroundColor: Colors.pink[500], backgroundColor: Colors.pink[500],
), ) : null,
); );
} }
} }

View File

@ -79,6 +79,8 @@ class SettingsScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
AppModel model = Provider.of<AppModel>(context);
User? user = model.user;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('About Treadl'), title: Text('About Treadl'),
@ -97,6 +99,8 @@ class SettingsScreen extends StatelessWidget {
SizedBox(height: 30), SizedBox(height: 30),
user != null ? Column(
children: [
ListTile( ListTile(
leading: Icon(Icons.exit_to_app), leading: Icon(Icons.exit_to_app),
title: Text('Logout'), title: Text('Logout'),
@ -107,6 +111,12 @@ class SettingsScreen extends StatelessWidget {
title: Text('Delete Account'), title: Text('Delete Account'),
onTap: () => _deleteAccount(context), onTap: () => _deleteAccount(context),
), ),
]
) : CupertinoButton(
color: Colors.pink,
child: Text('Join Treadl', style: TextStyle(color: Colors.white)),
onPressed: () => context.push('/welcome'),
),
SizedBox(height: 30), SizedBox(height: 30),