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'])
if not owner and proj['visibility'] != 'public':
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']:
obj['previewUrl'] = uploads.get_presigned_url('projects/{0}/{1}'.format(proj['_id'], 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';
class _GroupScreenState extends State<GroupScreen> {
final String id;
Map<String, dynamic>? _group;
int _selectedIndex = 0;
List<Widget> _widgetOptions = <Widget> [];
final Map<String, dynamic> _group;
_GroupScreenState(this._group) {
_widgetOptions = <Widget> [
GroupNoticeBoardTab(this._group),
GroupMembersTab(this._group)
];
_GroupScreenState(this.id) { }
@override
void initState() {
fetchGroup();
super.initState();
}
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
void fetchGroup() async {
Api api = Api();
var data = await api.request('GET', '/groups/' + id);
if (data['success'] == true) {
setState(() {
_group = data['payload'];
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_group['name'])
title: Text(_group?['name'] ?? 'Group')
),
body: Center(
child: _widgetOptions.elementAt(_selectedIndex),
child: _group != null ?
[
GroupNoticeBoardTab(_group!),
GroupMembersTab(_group!)
].elementAt(_selectedIndex)
: CircularProgressIndicator(),
),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
@ -45,15 +55,17 @@ class _GroupScreenState extends State<GroupScreen> {
],
currentIndex: _selectedIndex,
selectedItemColor: Colors.pink[600],
onTap: _onItemTapped,
onTap: (int index) => setState(() {
_selectedIndex = index;
}),
),
);
}
}
class GroupScreen extends StatefulWidget {
final Map<String,dynamic> group;
GroupScreen(this.group) { }
final String id;
GroupScreen(this.id) { }
@override
_GroupScreenState createState() => _GroupScreenState(group);
_GroupScreenState createState() => _GroupScreenState(id);
}

View File

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

View File

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

View File

@ -13,7 +13,9 @@ import 'register.dart';
import 'onboarding.dart';
import 'home.dart';
import 'project.dart';
import 'object.dart';
import 'settings.dart';
import 'group.dart';
final router = GoRouter(
routes: [
@ -37,7 +39,9 @@ final router = GoRouter(
GoRoute(path: '/onboarding', builder: (context, state) => OnboardingScreen()),
GoRoute(path: '/home', builder: (context, state) => HomeScreen()),
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;
SharedPreferences prefs = await SharedPreferences.getInstance();
String? token = prefs.getString('apiToken');
print('Nooo');
if (token != null) {
print('HEE');
AppModel model = Provider.of<AppModel>(context, listen: false);
await model.setToken(token!);
FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;

View File

@ -3,6 +3,7 @@ import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:go_router/go_router.dart';
import 'dart:io';
import 'api.dart';
import 'util.dart';
@ -10,30 +11,32 @@ import 'patterns/pattern.dart';
import 'patterns/viewer.dart';
class _ObjectScreenState extends State<ObjectScreen> {
final Map<String,dynamic> _project;
Map<String,dynamic> _object;
Map<String,dynamic>? _pattern;
final String username;
final String projectPath;
final String id;
final Map<String,dynamic>? project;
Map<String,dynamic>? object;
Map<String,dynamic>? pattern;
bool _isLoading = false;
final Function? onUpdate;
final Function? onDelete;
final Api api = Api();
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
initState() {
super.initState();
if (_object['type'] == 'pattern') {
_fetchPattern();
}
fetchObject();
}
void _fetchPattern() async {
var data = await api.request('GET', '/objects/' + _object['_id']);
void fetchObject() async {
var data = await api.request('GET', '/objects/' + id);
if (data['success'] == true) {
setState(() {
_pattern = data['payload']['pattern'];
object = data['payload'];
pattern = data['payload']['pattern'];
});
}
}
@ -41,14 +44,14 @@ class _ObjectScreenState extends State<ObjectScreen> {
void _shareObject() async {
setState(() => _isLoading = true);
File? file;
if (_object['type'] == 'pattern') {
var data = await api.request('GET', '/objects/' + _object['_id'] + '/wif');
if (object!['type'] == 'pattern') {
var data = await api.request('GET', '/objects/' + id + '/wif');
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 {
String fileName = Uri.file(_object['url']).pathSegments.last;
file = await api.downloadFile(_object['url'], fileName);
String fileName = Uri.file(object!['url']).pathSegments.last;
file = await api.downloadFile(object!['url'], fileName);
}
if (file != null) {
@ -58,12 +61,10 @@ class _ObjectScreenState extends State<ObjectScreen> {
}
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']) {
Navigator.pop(context);
Navigator.pop(modalContext);
Navigator.pop(context);
onDelete!(_object['_id']);
context.go('/home');
onDelete!(id);
}
}
@ -77,7 +78,7 @@ class _ObjectScreenState extends State<ObjectScreen> {
CupertinoDialogAction(
isDefaultAction: true,
child: Text('No'),
onPressed: () => Navigator.pop(context),
onPressed: () => context.pop(),
),
CupertinoDialogAction(
isDestructiveAction: true,
@ -105,22 +106,22 @@ class _ObjectScreenState extends State<ObjectScreen> {
TextButton(
child: Text('CANCEL'),
onPressed: () {
Navigator.pop(context);
context.pop();
},
),
TextButton(
child: Text('OK'),
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']) {
Navigator.pop(context);
_object['name'] = data['payload']['name'];
onUpdate!(_object['_id'], data['payload']);
context.pop();
object!['name'] = data['payload']['name'];
onUpdate!(id, data['payload']);
setState(() {
_object = _object;
object = object;
});
}
Navigator.pop(context);
context.pop();
},
),
],
@ -136,7 +137,7 @@ class _ObjectScreenState extends State<ObjectScreen> {
return CupertinoActionSheet(
title: Text('Manage this object'),
cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.of(modalContext).pop(),
onPressed: () => modalContext.pop(),
child: Text('Cancel')
),
actions: [
@ -156,15 +157,22 @@ class _ObjectScreenState extends State<ObjectScreen> {
}
Widget getObjectWidget() {
if (_object['isImage'] == true) {
return Image.network(_object['url']);
if (object == null) {
return Center(child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [CircularProgressIndicator()]
));
}
else if (_object['type'] == 'pattern') {
if (_pattern != null) {
return PatternViewer(_pattern!, withEditor: true);
else if (object!['isImage'] == true && object!['url'] != null) {
print(object!['url']);
return Image.network(object!['url']);
}
else if (object!['type'] == 'pattern') {
if (pattern != null) {
return PatternViewer(pattern!, withEditor: true);
}
else if (_object['previewUrl'] != null) {
return Image.network(_object['previewUrl']!);;
else if (object!['previewUrl'] != null) {
return Image.network(object!['previewUrl']!);;
}
else {
return Column(
@ -184,7 +192,7 @@ class _ObjectScreenState extends State<ObjectScreen> {
Text('Treadl cannot display this type of item.'),
SizedBox(height: 20),
ElevatedButton(child: Text('View file'), onPressed: () {
launch(_object['url']);
launch(object!['url']);
}),
],
));
@ -194,11 +202,11 @@ class _ObjectScreenState extends State<ObjectScreen> {
@override
Widget build(BuildContext context) {
String description = '';
if (_object['description'] != null)
description = _object['description'];
if (object?['description'] != null)
description = object!['description']!;
return Scaffold(
appBar: AppBar(
title: Text(_object['name']),
title: Text(object?['name'] ?? 'Object'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.ios_share),
@ -228,12 +236,15 @@ class _ObjectScreenState extends State<ObjectScreen> {
}
class ObjectScreen extends StatefulWidget {
final Map<String,dynamic> _object;
final Map<String,dynamic> _project;
final String username;
final String projectPath;
final String id;
final Map<String,dynamic>? object;
final Map<String,dynamic>? project;
final Function? onUpdate;
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
_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(
color: Colors.white,
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 'api.dart';
import 'util.dart';
import 'object.dart';
import 'model.dart';
class _ProjectScreenState extends State<ProjectScreen> {
final String username;
@ -252,12 +252,7 @@ class _ProjectScreenState extends State<ProjectScreen> {
return new Card(
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ObjectScreen(object, project!, onUpdate: _onUpdateObject, onDelete: _onDeleteObject),
),
);
context.push('/' + username + '/' + projectPath + '/' + object['_id']);
},
child: ListTile(
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
Widget build(BuildContext context) {
AppModel model = Provider.of<AppModel>(context);
User? user = model.user;
return Scaffold(
appBar: AppBar(
title: Text(project?['name'] ?? 'Project'),
@ -289,33 +307,13 @@ class _ProjectScreenState extends State<ProjectScreen> {
) : SizedBox(width: 0),
]
),
body: _loading ?
Container(
margin: const EdgeInsets.all(10.0),
alignment: Alignment.center,
child: CircularProgressIndicator()
)
: Container(
body: 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),
])
alignment: Alignment.center,
child: getBody(),
),
floatingActionButtonLocation: ExpandableFab.location,
floatingActionButton: ExpandableFab(
floatingActionButton: user != null ? ExpandableFab(
distance: 70,
type: ExpandableFabType.up,
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 {
AppModel model = Provider.of<AppModel>(context, listen: false);
if (model.user == null) return;
setState(() {
_loading = true;
});
@ -131,6 +133,8 @@ class _ProjectsTabState extends State<ProjectsTab> {
@override
Widget build(BuildContext context) {
AppModel model = Provider.of<AppModel>(context);
User? user = model.user;
return Scaffold(
appBar: AppBar(
title: Text('My Projects'),
@ -148,11 +152,11 @@ class _ProjectsTabState extends State<ProjectsTab> {
alignment: Alignment.center,
child: getBody()
),
floatingActionButton: FloatingActionButton(
floatingActionButton: user != null ? FloatingActionButton(
onPressed: showNewProjectDialog,
child: _creatingProject ? CircularProgressIndicator(backgroundColor: Colors.white) : Icon(Icons.add),
backgroundColor: Colors.pink[500],
),
) : null,
);
}
}

View File

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