Compare commits

..

No commits in common. "a22c2d7d16d7358103009d5ef77f99736bad1f67" and "6e15952ffc57f0b0c1a5d5cd2cdc446bef9724ba" have entirely different histories.

33 changed files with 433 additions and 967 deletions

View File

@ -29,14 +29,11 @@ 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']
if obj.get('fullPreview'): if obj.get('fullPreview'):
obj['fullPreviewUrl'] = uploads.get_presigned_url('projects/{0}/{1}'.format(proj['_id'], obj['fullPreview'])) obj['fullPreviewUrl'] = uploads.get_presigned_url('projects/{0}/{1}'.format(proj['_id'], obj['fullPreview']))
obj['projectObject'] = proj
return obj return obj
def copy_to_project(user, id, project_id): def copy_to_project(user, id, project_id):

View File

@ -61,11 +61,8 @@ def discover(user, count = 3):
random.shuffle(all_projects) random.shuffle(all_projects)
for p in all_projects: for p in all_projects:
if db.objects.find_one({'project': p['_id'], 'name': {'$ne': 'Untitled pattern'}}): if db.objects.find_one({'project': p['_id'], 'name': {'$ne': 'Untitled pattern'}}):
owner = db.users.find_one({'_id': p['user']}, {'username': 1, 'avatar': 1}) owner = db.users.find_one({'_id': p['user']}, {'username': 1})
p['fullName'] = owner['username'] + '/' + p['path'] p['fullName'] = owner['username'] + '/' + p['path']
p['owner'] = owner
if 'avatar' in p['owner']:
p['owner']['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(p['owner']['_id'], p['owner']['avatar']))
projects.append(p) projects.append(p)
if len(projects) >= count: break if len(projects) >= count: break
@ -98,7 +95,7 @@ def explore(page = 1):
all_public_project_ids = list(map(lambda p: p['_id'], all_public_projects)) all_public_project_ids = list(map(lambda p: p['_id'], all_public_projects))
for project in all_public_projects: for project in all_public_projects:
project_map[project['_id']] = project project_map[project['_id']] = project
objects = list(db.objects.find({'project': {'$in': all_public_project_ids}, 'name': {'$not': re.compile('untitled pattern', re.IGNORECASE)}, 'preview': {'$exists': True}}, {'project': 1, 'name': 1, 'createdAt': 1, 'type': 1, 'preview': 1}).sort('createdAt', pymongo.DESCENDING).skip((page - 1) * per_page).limit(per_page)) objects = list(db.objects.find({'project': {'$in': all_public_project_ids}, 'name': {'$not': re.compile('untitled pattern', re.IGNORECASE)}, 'preview': {'$exists': True}}, {'project': 1, 'name': 1, 'createdAt': 1, 'preview': 1}).sort('createdAt', pymongo.DESCENDING).skip((page - 1) * per_page).limit(per_page))
for object in objects: for object in objects:
object['projectObject'] = project_map.get(object['project']) object['projectObject'] = project_map.get(object['project'])
if 'preview' in object and '.png' in object['preview']: if 'preview' in object and '.png' in object['preview']:

View File

@ -30,22 +30,13 @@ def get(user, username):
if not user or not user['_id'] == fetch_user['_id']: if not user or not user['_id'] == fetch_user['_id']:
project_query['visibility'] = 'public' project_query['visibility'] = 'public'
fetch_user['projects'] = list(db.projects.find(project_query, {'name': 1, 'path': 1, 'description': 1, 'visibility': 1}).limit(15))
for project in fetch_user['projects']:
project['fullName'] = fetch_user['username'] + '/' + project['path']
if 'avatar' in fetch_user: if 'avatar' in fetch_user:
fetch_user['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(str(fetch_user['_id']), fetch_user['avatar'])) fetch_user['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(str(fetch_user['_id']), fetch_user['avatar']))
if user: if user:
fetch_user['following'] = fetch_user['_id'] in list(map(lambda f: f['user'], user.get('following', []))) fetch_user['following'] = fetch_user['_id'] in list(map(lambda f: f['user'], user.get('following', [])))
user_projects = list(db.projects.find(project_query, {'name': 1, 'path': 1, 'description': 1, 'visibility': 1}).limit(15))
for project in user_projects:
project['fullName'] = fetch_user['username'] + '/' + project['path']
project['owner'] = {
'_id': fetch_user['_id'],
'username': fetch_user['username'],
'avatar': fetch_user.get('avatar'),
'avatarUrl': fetch_user.get('avatarUrl'),
}
fetch_user['projects'] = user_projects
return fetch_user return fetch_user
def update(user, username, data): def update(user, username, data):

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

View File

@ -48,7 +48,6 @@
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9C430D344D81D00E4F8BC572 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9C430D344D81D00E4F8BC572 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BE18F7F22B54707500363B2E /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
BE6C8E7324CDE9B20018AD10 /* RunnerDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerDebug.entitlements; sourceTree = "<group>"; }; BE6C8E7324CDE9B20018AD10 /* RunnerDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerDebug.entitlements; sourceTree = "<group>"; };
BEA6727A24CCAF5600BBF836 /* RunnerRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerRelease.entitlements; sourceTree = "<group>"; }; BEA6727A24CCAF5600BBF836 /* RunnerRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerRelease.entitlements; sourceTree = "<group>"; };
BEA6727B24CCB04900BBF836 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; }; BEA6727B24CCB04900BBF836 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
@ -117,7 +116,6 @@
97C146F01CF9000F007C117D /* Runner */ = { 97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
BE18F7F22B54707500363B2E /* Runner.entitlements */,
BE6C8E7324CDE9B20018AD10 /* RunnerDebug.entitlements */, BE6C8E7324CDE9B20018AD10 /* RunnerDebug.entitlements */,
BEA6727A24CCAF5600BBF836 /* RunnerRelease.entitlements */, BEA6727A24CCAF5600BBF836 /* RunnerRelease.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FA1CF9000F007C117D /* Main.storyboard */,
@ -375,7 +373,6 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 38T664W57F; DEVELOPMENT_TEAM = 38T664W57F;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1520" LastUpgradeVersion = "1430"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@ -12,8 +12,6 @@
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>Treadl</string> <string>Treadl</string>
<key>FlutterDeepLinkingEnabled</key>
<true/>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:treadl.com</string>
<string>applinks:www.treadl.com</string>
</array>
</dict>
</plist>

View File

@ -4,10 +4,5 @@
<dict> <dict>
<key>aps-environment</key> <key>aps-environment</key>
<string>development</string> <string>development</string>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:treadl.com</string>
<string>applinks:www.treadl.com</string>
</array>
</dict> </dict>
</plist> </plist>

View File

@ -4,10 +4,5 @@
<dict> <dict>
<key>aps-environment</key> <key>aps-environment</key>
<string>development</string> <string>development</string>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:treadl.com</string>
<string>applinks:www.treadl.com</string>
</array>
</dict> </dict>
</plist> </plist>

View File

@ -1,21 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:provider/provider.dart';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'util.dart'; import 'util.dart';
import 'model.dart';
class Api { class Api {
String? _token; String? _token;
final String apiBase = 'https://api.treadl.com'; //final String apiBase = 'https://api.treadl.com';
//final String apiBase = 'http://192.168.5.134:2001'; final String apiBase = 'http://192.168.5.134:2001';
Api({token: null}) {
if (token != null) _token = token;
}
Future<String?> loadToken() async { Future<String?> loadToken() async {
if (_token != null) { if (_token != null) {

View File

@ -1,89 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import 'routeArguments.dart';
import 'api.dart';
import 'util.dart';
import 'lib.dart';
class _ExploreTabState extends State<ExploreTab> {
List<dynamic> objects = [];
List<dynamic> projects = [];
bool loading = false;
final Api api = Api();
final Util util = Util();
@override
initState() {
super.initState();
getData();
}
void getData() async {
setState(() {
loading = true;
});
var data = await api.request('GET', '/search/explore');
if (data['success'] == true) {
setState(() {
loading = false;
objects = data['payload']['objects'];
});
}
var data2 = await api.request('GET', '/search/discover');
if (data2['success'] == true) {
setState(() {
projects = data2['payload']['highlightProjects'];
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Explore'),
),
body: loading ?
Container(
margin: const EdgeInsets.all(10.0),
alignment: Alignment.center,
child: CircularProgressIndicator()
)
: Container(
color: Color.fromRGBO(255, 251, 248, 1),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 10),
CustomText('Discover projects', 'h1', margin: 5),
Container(
height: 130,
child: ListView(
scrollDirection: Axis.horizontal,
children: projects.map((p) => ProjectCard(p)).toList()
)
),
SizedBox(height: 10),
CustomText('Recent patterns', 'h1', margin: 5),
Expanded(child: GridView.count(
crossAxisCount: 2,
mainAxisSpacing: 5,
crossAxisSpacing: 5,
childAspectRatio: 0.9,
children: objects.map((object) =>
PatternCard(object)
).toList(),
)),
]
)
),
);
}
}
class ExploreTab extends StatefulWidget {
@override
_ExploreTabState createState() => _ExploreTabState();
}

View File

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

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart';
import 'api.dart'; import 'api.dart';
import 'util.dart'; import 'util.dart';
import 'group_noticeboard.dart';
import 'user.dart'; import 'user.dart';
class _GroupMembersTabState extends State<GroupMembersTab> { class _GroupMembersTabState extends State<GroupMembersTab> {
@ -33,7 +33,14 @@ class _GroupMembersTabState extends State<GroupMembersTab> {
Widget getMemberCard(member) { Widget getMemberCard(member) {
return new ListTile( return new ListTile(
onTap: () => context.push('/' + member['username']), onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => UserScreen(member),
),
);
},
leading: util.avatarImage(util.avatarUrl(member), size: 40), leading: util.avatarImage(util.avatarUrl(member), size: 40),
trailing: Icon(Icons.keyboard_arrow_right), trailing: Icon(Icons.keyboard_arrow_right),
title: Text(member['username']) title: Text(member['username'])

View File

@ -1,11 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'util.dart';
import 'api.dart'; import 'api.dart';
import 'user.dart';
import 'lib.dart'; import 'lib.dart';
class _GroupNoticeBoardTabState extends State<GroupNoticeBoardTab> { class _GroupNoticeBoardTabState extends State<GroupNoticeBoardTab> {
final TextEditingController _newEntryController = TextEditingController(); final TextEditingController _newEntryController = TextEditingController();
final Util utils = new Util();
final Api api = Api(); final Api api = Api();
Map<String,dynamic> _group; Map<String,dynamic> _group;
List<dynamic> _entries = []; List<dynamic> _entries = [];
@ -38,10 +42,8 @@ class _GroupNoticeBoardTabState extends State<GroupNoticeBoardTab> {
} }
void _sendPost(context) async { void _sendPost(context) async {
String text = _newEntryController.text;
if (text.length == 0) return;
setState(() => _posting = true); setState(() => _posting = true);
var data = await api.request('POST', '/groups/' + _group['_id'] + '/entries', {'content': text}); var data = await api.request('POST', '/groups/' + _group['_id'] + '/entries', {'content': _newEntryController.text});
if (data['success'] == true) { if (data['success'] == true) {
_newEntryController.value = TextEditingValue(text: ''); _newEntryController.value = TextEditingValue(text: '');
FocusScope.of(context).requestFocus(FocusNode()); FocusScope.of(context).requestFocus(FocusNode());

View File

@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'dart:convert';
import 'package:go_router/go_router.dart'; import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'group.dart';
import 'api.dart'; import 'api.dart';
import 'model.dart';
import 'lib.dart';
class _GroupsTabState extends State<GroupsTab> { class _GroupsTabState extends State<GroupsTab> {
List<dynamic> _groups = []; List<dynamic> _groups = [];
@ -16,8 +16,6 @@ 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');
@ -38,7 +36,14 @@ class _GroupsTabState extends State<GroupsTab> {
} }
return Card( return Card(
child: InkWell( child: InkWell(
onTap: () => context.push('/groups/' + group['_id']), onTap: () {
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),
@ -50,42 +55,38 @@ class _GroupsTabState extends State<GroupsTab> {
; ;
} }
Widget getBody() {
AppModel model = Provider.of<AppModel>(context);
if (model.user == null)
return LoginNeeded(text: 'Once logged in, you\'ll find your groups here.');
else if (_loading)
return CircularProgressIndicator();
else if (_groups != null && _groups.length > 0)
return ListView.builder(
itemCount: _groups.length,
itemBuilder: (BuildContext context, int index) {
return buildGroupCard(_groups[index]);
},
);
else
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('You aren\'t a member of any groups yet', style: TextStyle(fontSize: 20), textAlign: TextAlign.center),
Image(image: AssetImage('assets/group.png'), width: 300),
Text('Groups let you meet and keep in touch with others in the weaving community.', textAlign: TextAlign.center),
Text('Please use our website to join and leave groups.', textAlign: TextAlign.center),
]);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('My Groups'), title: Text('Groups'),
), ),
body: Container( body: _loading ?
Container(
margin: const EdgeInsets.all(10.0),
alignment: Alignment.center,
child: CircularProgressIndicator()
)
: Container(
margin: const EdgeInsets.all(10.0), margin: const EdgeInsets.all(10.0),
alignment: Alignment.center, child: (_groups != null && _groups.length > 0) ?
child: getBody() ListView.builder(
) itemCount: _groups.length,
itemBuilder: (BuildContext context, int index) {
return buildGroupCard(_groups[index]);
},
)
:
Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('You aren\'t a member of any groups yet', style: TextStyle(fontSize: 20), textAlign: TextAlign.center),
Image(image: AssetImage('assets/group.png'), width: 300),
Text('Groups let you meet and keep in touch with others in the weaving community.', textAlign: TextAlign.center),
Text('Please use our website to join and leave groups.', textAlign: TextAlign.center),
])
),
); );
} }
} }

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'explore.dart';
import 'projects.dart'; import 'projects.dart';
import 'groups.dart'; import 'groups.dart';
@ -14,7 +13,6 @@ class HomeScreen extends StatefulWidget {
class _MyStatefulWidgetState extends State<HomeScreen> { class _MyStatefulWidgetState extends State<HomeScreen> {
int _selectedIndex = 0; int _selectedIndex = 0;
List<Widget> _widgetOptions = <Widget> [ List<Widget> _widgetOptions = <Widget> [
ExploreTab(),
ProjectsTab(), ProjectsTab(),
GroupsTab() GroupsTab()
]; ];
@ -33,17 +31,13 @@ class _MyStatefulWidgetState extends State<HomeScreen> {
), ),
bottomNavigationBar: BottomNavigationBar( bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[ items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.explore),
label: 'Explore',
),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.folder), icon: Icon(Icons.folder),
label: 'My Projects', label: 'Projects',
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.person), icon: Icon(Icons.person),
label: 'My Groups', label: 'Groups',
), ),
], ],
currentIndex: _selectedIndex, currentIndex: _selectedIndex,

View File

@ -4,12 +4,9 @@ import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:go_router/go_router.dart';
import 'api.dart'; import 'api.dart';
import 'util.dart'; import 'util.dart';
import 'user.dart'; import 'user.dart';
import 'object.dart';
import 'project.dart';
class Alert extends StatelessWidget { class Alert extends StatelessWidget {
final String type; final String type;
@ -103,7 +100,7 @@ class _NoticeboardPostState extends State<NoticeboardPost> {
if (onDelete != null) { if (onDelete != null) {
onDelete!(_entry); onDelete!(_entry);
} }
context.pop(); Navigator.of(context).pop();
} }
} }
@ -149,7 +146,7 @@ class _NoticeboardPostState extends State<NoticeboardPost> {
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
context.pop(); Navigator.of(context).pop();
}, },
child: Text('Cancel'), child: Text('Cancel'),
) )
@ -168,7 +165,11 @@ class _NoticeboardPostState extends State<NoticeboardPost> {
Row( Row(
children: <Widget>[ children: <Widget>[
GestureDetector( GestureDetector(
onTap: () => context.push('/' + _entry['authorUser']['username']), onTap: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) => UserScreen(_entry['authorUser']),
));
},
child: utils.avatarImage(utils.avatarUrl(_entry['authorUser']), size: isReply ? 30 : 40) child: utils.avatarImage(utils.avatarUrl(_entry['authorUser']), size: isReply ? 30 : 40)
), ),
SizedBox(width: 5), SizedBox(width: 5),
@ -228,172 +229,3 @@ class NoticeboardInput extends StatelessWidget {
} }
} }
class UserChip extends StatelessWidget {
final Map<String,dynamic> user;
final Util utils = new Util();
UserChip(this.user) {}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => context.push('/' + user['username']),
child: Row(
children: [
utils.avatarImage(utils.avatarUrl(user), size: 20),
SizedBox(width: 5),
Text(user['username'], style: TextStyle(color: Colors.grey))
]
)
);
}
}
class PatternCard extends StatelessWidget {
final Map<String,dynamic> object;
PatternCard(this.object) {}
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6.0),
),
child: InkWell(
onTap: () {
context.push('/' + object['userObject']['username'] + '/' + object['projectObject']['path'] + '/' + object['_id']);
},
child: Column(
children: [
Container(
height: 100,
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
image: NetworkImage(object['previewUrl']),
),
),
),
Container(
padding: EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
UserChip(object['userObject']),
SizedBox(height: 5),
Text(Util.ellipsis(object['name'], 35), style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold)),
]
)
)
]
)
)
);
}
}
class ProjectCard extends StatelessWidget {
final Map<String,dynamic> project;
ProjectCard(this.project) {}
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6.0),
),
child: InkWell(
onTap: () {
context.push('/' + this.project['owner']['username'] + '/' + this.project['path']);
},
child: Column(
children: [
Container(
padding: EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.folder),
SizedBox(height: 10),
UserChip(project['owner']),
SizedBox(height: 5),
Text(Util.ellipsis(project['name'], 35), style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold)),
]
)
)
]
)
)
);
}
}
class CustomText extends StatelessWidget {
final String text;
final String type;
final double margin;
TextStyle? style;
CustomText(this.text, this.type, {this.margin = 0}) {
if (this.type == 'h1') {
style = TextStyle(fontSize: 30, fontWeight: FontWeight.bold);
}
else {
style = TextStyle();
}
}
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(this.margin),
child: Text(text, style: style)
);
}
}
class LoginNeeded extends StatelessWidget {
final String? text;
LoginNeeded({this.text}) {}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('You need to login to see this', style: TextStyle(fontSize: 20), textAlign: TextAlign.center),
Image(image: AssetImage('assets/login.png'), width: 300),
text != null ? Text(text!, textAlign: TextAlign.center) : SizedBox(height: 10),
CupertinoButton(
onPressed: () {
context.push('/welcome');
},
child: new Text("Login or register",
textAlign: TextAlign.center,
)
)
]
);
}
}
class EmptyBox extends StatelessWidget {
final String title;
final String? description;
EmptyBox(this.title, {this.description}) {}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(title, style: TextStyle(fontSize: 20), textAlign: TextAlign.center),
Image(image: AssetImage('assets/empty.png'), width: 300),
description != null ? Text('Add a pattern file, an image, or something else to this project using the + button below.', textAlign: TextAlign.center) : SizedBox(height: 0),
]);
}
}

View File

@ -1,11 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart';
import 'api.dart'; import 'api.dart';
import 'model.dart';
class _LoginScreenState extends State<LoginScreen> { class _LoginScreenState extends State<LoginScreen> {
final TextEditingController _emailController = TextEditingController(); final TextEditingController _emailController = TextEditingController();
@ -13,14 +11,15 @@ class _LoginScreenState extends State<LoginScreen> {
final Api api = Api(); final Api api = Api();
bool _loggingIn = false; bool _loggingIn = false;
void _submit(BuildContext context) async { void _submit(context) async {
setState(() => _loggingIn = true); setState(() => _loggingIn = true);
var data = await api.request('POST', '/accounts/login', {'email': _emailController.text, 'password': _passwordController.text}); var data = await api.request('POST', '/accounts/login', {'email': _emailController.text, 'password': _passwordController.text});
setState(() => _loggingIn = false); setState(() => _loggingIn = false);
if (data['success'] == true) { if (data['success'] == true) {
AppModel model = Provider.of<AppModel>(context, listen: false); String token = data['payload']['token'];
await model.setToken(data['payload']['token']); SharedPreferences prefs = await SharedPreferences.getInstance();
context.go('/onboarding'); prefs.setString('apiToken', token);
Navigator.of(context).pushNamedAndRemoveUntil('/onboarding', (Route<dynamic> route) => false);
} }
else { else {
showDialog( showDialog(
@ -32,7 +31,7 @@ class _LoginScreenState extends State<LoginScreen> {
CupertinoDialogAction( CupertinoDialogAction(
isDefaultAction: true, isDefaultAction: true,
child: Text('Try again'), child: Text('Try again'),
onPressed: () => context.pop(), onPressed: () => Navigator.pop(context),
), ),
], ],
) )
@ -47,17 +46,19 @@ class _LoginScreenState extends State<LoginScreen> {
title: Text('Login to Treadl'), title: Text('Login to Treadl'),
), ),
body: Container( body: Container(
margin: const EdgeInsets.only(top: 40, left: 10, right: 10), margin: const EdgeInsets.all(10.0),
child: ListView( child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Text('Login with your Treadl account', style: TextStyle(fontSize: 20)), Image(image: AssetImage('assets/logo.png'), width: 100),
SizedBox(height: 30), SizedBox(height: 20),
Text('Login using your Treadl account.', style: TextStyle(fontSize: 18)),
SizedBox(height: 20),
TextField( TextField(
autofocus: true, autofocus: true,
controller: _emailController, controller: _emailController,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'sam@example.com', labelText: 'Email address or username', hintText: 'sam@example.com', labelText: 'Email address or username'
border: OutlineInputBorder(),
), ),
), ),
SizedBox(height: 10), SizedBox(height: 10),
@ -66,8 +67,7 @@ class _LoginScreenState extends State<LoginScreen> {
controller: _passwordController, controller: _passwordController,
obscureText: true, obscureText: true,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Type your password', labelText: 'Your password', hintText: 'Type your password', labelText: 'Your password'
border: OutlineInputBorder(),
), ),
), ),
SizedBox(height: 5), SizedBox(height: 5),

View File

@ -3,54 +3,19 @@ import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:go_router/go_router.dart';
//import 'package:fluttertoast/fluttertoast.dart'; //import 'package:fluttertoast/fluttertoast.dart';
import 'api.dart'; import 'api.dart';
import 'model.dart'; import 'store.dart';
import 'welcome.dart'; import 'welcome.dart';
import 'login.dart'; import 'login.dart';
import 'register.dart'; import 'register.dart';
import 'onboarding.dart'; import 'onboarding.dart';
import 'home.dart'; import 'home.dart';
import 'project.dart';
import 'object.dart';
import 'settings.dart';
import 'group.dart';
import 'user.dart';
final router = GoRouter(
routes: [
GoRoute(path: '/', builder: (context, state) => Startup()),
GoRoute(path: '/welcome', pageBuilder: (context, state) {
return CustomTransitionPage(
key: state.pageKey,
child: WelcomeScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// Change the opacity of the screen using a Curve based on the the animation's value
return FadeTransition(
opacity:
CurveTween(curve: Curves.easeInOutCirc).animate(animation),
child: child,
);
},
);
}),
GoRoute(path: '/login', builder: (context, state) => LoginScreen()),
GoRoute(path: '/register', builder: (context, state) => RegisterScreen()),
GoRoute(path: '/onboarding', builder: (context, state) => OnboardingScreen()),
GoRoute(path: '/home', builder: (context, state) => HomeScreen()),
GoRoute(path: '/settings', builder: (context, state) => SettingsScreen()),
GoRoute(path: '/groups/:id', builder: (context, state) => GroupScreen(state.pathParameters['id']!)),
GoRoute(path: '/:username', builder: (context, state) => UserScreen(state.pathParameters['username']!)),
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']!)),
],
);
void main() { void main() {
runApp( runApp(
ChangeNotifierProvider( ChangeNotifierProvider(
create: (context) => AppModel(), create: (context) => Store(),
child: MyApp() child: MyApp()
) )
); );
@ -72,19 +37,21 @@ class _AppState extends State<MyApp> {
// Initialize FlutterFire: // Initialize FlutterFire:
future: _initialization, future: _initialization,
builder: (context, snapshot) { builder: (context, snapshot) {
return MaterialApp.router( return MaterialApp(
routerConfig: router,
/*return MaterialApp(*/
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
title: 'Treadl', title: 'Treadl',
theme: ThemeData( theme: ThemeData(
primarySwatch: Colors.pink, primarySwatch: Colors.pink,
//textSelectionColor: Colors.blue, //textSelectionColor: Colors.blue,
), ),
/*home: Startup(), home: Startup(),
routes: <String, WidgetBuilder>{ routes: <String, WidgetBuilder>{
'/welcome': (BuildContext context) => WelcomeScreen(),
}*/ '/login': (BuildContext context) => LoginScreen(),
'/register': (BuildContext context) => RegisterScreen(),
'/onboarding': (BuildContext context) => OnboardingScreen(),
'/home': (BuildContext context) => HomeScreen(),
}
); );
}, },
); );
@ -121,8 +88,8 @@ class Startup extends StatelessWidget {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
String? token = prefs.getString('apiToken'); String? token = prefs.getString('apiToken');
if (token != null) { if (token != null) {
AppModel model = Provider.of<AppModel>(context, listen: false); Provider.of<Store>(context, listen: false).setToken(token!);
await model.setToken(token!);
FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance; FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;
await _firebaseMessaging.requestPermission( await _firebaseMessaging.requestPermission(
alert: true, alert: true,
@ -139,8 +106,11 @@ class Startup extends StatelessWidget {
Api api = Api(); Api api = Api();
api.request('PUT', '/accounts/pushToken', {'pushToken': _pushToken!}); api.request('PUT', '/accounts/pushToken', {'pushToken': _pushToken!});
} }
// Push without including current route in stack:
Navigator.of(context, rootNavigator: true).pushNamedAndRemoveUntil('/home', (Route<dynamic> route) => false);
} else {
Navigator.of(context).pushNamedAndRemoveUntil('/welcome', (Route<dynamic> route) => false);
} }
context.go('/home');
} }
@override @override

View File

@ -1,65 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'api.dart';
class User {
final String id;
final String username;
String? avatar;
String? avatarUrl;
User(this.id, this.username, {this.avatar, this.avatarUrl}) {}
static User loadJSON(Map<String,dynamic> input) {
return User(input['_id'], input['username'], avatar: input['avatar'], avatarUrl: input['avatarUrl']);
}
}
class AppModel extends ChangeNotifier {
User? user;
void setUser(User? u) {
user = u;
notifyListeners();
}
String? apiToken;
Future<void> setToken(String? newToken) async {
apiToken = newToken;
SharedPreferences prefs = await SharedPreferences.getInstance();
if (apiToken != null) {
Api api = Api(token: apiToken!);
prefs.setString('apiToken', apiToken!);
var data = await api.request('GET', '/users/me');
if (data['success'] == true) {
setUser(User.loadJSON(data['payload']));
print(data);
}
} else {
prefs.remove('apiToken');
}
}
/*
/// Internal, private state of the cart.
final List<Item> _items = [];
/// An unmodifiable view of the items in the cart.
UnmodifiableListView<Item> get items => UnmodifiableListView(_items);
/// The current total price of all items (assuming all items cost $42).
int get totalPrice => _items.length * 42;
/// Adds [item] to cart. This and [removeAll] are the only ways to modify the
/// cart from the outside.
void add(Item item) {
_items.add(item);
// This call tells the widgets that are listening to this model to rebuild.
notifyListeners();
}
/// Removes all items from the cart.
void removeAll() {
_items.clear();
// This call tells the widgets that are listening to this model to rebuild.
notifyListeners();
}*/
}

View File

@ -3,38 +3,37 @@ 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';
import 'model.dart';
import 'patterns/pattern.dart'; import 'patterns/pattern.dart';
import 'patterns/viewer.dart'; import 'patterns/viewer.dart';
class _ObjectScreenState extends State<ObjectScreen> { class _ObjectScreenState extends State<ObjectScreen> {
final String username; final Map<String,dynamic> _project;
final String projectPath; Map<String,dynamic> _object;
final String id; Map<String,dynamic>? _pattern;
Map<String,dynamic>? object;
Map<String,dynamic>? pattern;
bool _isLoading = false; bool _isLoading = false;
final Function _onUpdate;
final Function _onDelete;
final Api api = Api(); final Api api = Api();
final Util util = Util(); final Util util = Util();
_ObjectScreenState(this.username, this.projectPath, this.id) { } _ObjectScreenState(this._object, this._project, this._onUpdate, this._onDelete) { }
@override @override
initState() { initState() {
super.initState(); super.initState();
fetchObject(); if (_object['type'] == 'pattern') {
_fetchPattern();
}
} }
void fetchObject() async { void _fetchPattern() async {
var data = await api.request('GET', '/objects/' + id); var data = await api.request('GET', '/objects/' + _object['_id']);
if (data['success'] == true) { if (data['success'] == true) {
setState(() { setState(() {
object = data['payload']; _pattern = data['payload']['pattern'];
pattern = data['payload']['pattern'];
}); });
} }
} }
@ -42,14 +41,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/' + id + '/wif'); var data = await api.request('GET', '/objects/' + _object['_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) {
@ -59,9 +58,12 @@ 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/' + id); var data = await api.request('DELETE', '/objects/' + _object['_id']);
if (data['success']) { if (data['success']) {
context.go('/home'); Navigator.pop(context);
Navigator.pop(modalContext);
Navigator.pop(context);
_onDelete(_object['_id']);
} }
} }
@ -75,7 +77,7 @@ class _ObjectScreenState extends State<ObjectScreen> {
CupertinoDialogAction( CupertinoDialogAction(
isDefaultAction: true, isDefaultAction: true,
child: Text('No'), child: Text('No'),
onPressed: () => context.pop(), onPressed: () => Navigator.pop(context),
), ),
CupertinoDialogAction( CupertinoDialogAction(
isDestructiveAction: true, isDestructiveAction: true,
@ -103,21 +105,22 @@ class _ObjectScreenState extends State<ObjectScreen> {
TextButton( TextButton(
child: Text('CANCEL'), child: Text('CANCEL'),
onPressed: () { onPressed: () {
context.pop(); Navigator.pop(context);
}, },
), ),
TextButton( TextButton(
child: Text('OK'), child: Text('OK'),
onPressed: () async { onPressed: () async {
var data = await api.request('PUT', '/objects/' + id, {'name': renameController.text}); var data = await api.request('PUT', '/objects/' + _object['_id'], {'name': renameController.text});
if (data['success']) { if (data['success']) {
context.pop(); Navigator.pop(context);
object!['name'] = data['payload']['name']; _object['name'] = data['payload']['name'];
_onUpdate(_object['_id'], data['payload']);
setState(() { setState(() {
object = object; _object = _object;
}); });
} }
context.pop(); Navigator.pop(context);
}, },
), ),
], ],
@ -133,7 +136,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: () => modalContext.pop(), onPressed: () => Navigator.of(modalContext).pop(),
child: Text('Cancel') child: Text('Cancel')
), ),
actions: [ actions: [
@ -153,22 +156,15 @@ class _ObjectScreenState extends State<ObjectScreen> {
} }
Widget getObjectWidget() { Widget getObjectWidget() {
if (object == null) { if (_object['isImage'] == true) {
return Center(child: Column( return Image.network(_object['url']);
crossAxisAlignment: CrossAxisAlignment.center,
children: [CircularProgressIndicator()]
));
} }
else if (object!['isImage'] == true && object!['url'] != null) { else if (_object['type'] == 'pattern') {
print(object!['url']); if (_pattern != null) {
return Image.network(object!['url']); return PatternViewer(_pattern!, withEditor: true);
}
else if (object!['type'] == 'pattern') {
if (pattern != null) {
return PatternViewer(pattern!, withEditor: true);
} }
else if (object!['previewUrl'] != null) { else if (_object['previewUrl'] != null) {
return Image.network(object!['previewUrl']!);; return Image.network(_object['previewUrl']!);;
} }
else { else {
return Column( return Column(
@ -188,7 +184,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']);
}), }),
], ],
)); ));
@ -197,14 +193,12 @@ class _ObjectScreenState extends State<ObjectScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
AppModel model = Provider.of<AppModel>(context);
User? user = model.user;
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'] ?? 'Object'), title: Text(_object['name']),
actions: <Widget>[ actions: <Widget>[
IconButton( IconButton(
icon: Icon(Icons.ios_share), icon: Icon(Icons.ios_share),
@ -212,12 +206,12 @@ class _ObjectScreenState extends State<ObjectScreen> {
_shareObject(); _shareObject();
}, },
), ),
Util.canEditProject(user, object?['projectObject']) ? IconButton( IconButton(
icon: Icon(Icons.settings), icon: Icon(Icons.settings),
onPressed: () { onPressed: () {
_showSettingsModal(context); _showSettingsModal(context);
}, },
) : SizedBox(height: 0), ),
] ]
), ),
body: Container( body: Container(
@ -234,11 +228,12 @@ class _ObjectScreenState extends State<ObjectScreen> {
} }
class ObjectScreen extends StatefulWidget { class ObjectScreen extends StatefulWidget {
final String username; final Map<String,dynamic> _object;
final String projectPath; final Map<String,dynamic> _project;
final String id; final Function _onUpdate;
ObjectScreen(this.username, this.projectPath, this.id, ) { } final Function _onDelete;
ObjectScreen(this._object, this._project, this._onUpdate, this._onDelete) { }
@override @override
_ObjectScreenState createState() => _ObjectScreenState(username, projectPath, id); _ObjectScreenState createState() => _ObjectScreenState(_object, _project, _onUpdate, _onDelete);
} }

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:go_router/go_router.dart';
import 'api.dart'; import 'api.dart';
class _OnboardingScreenState extends State<OnboardingScreen> { class _OnboardingScreenState extends State<OnboardingScreen> {
@ -112,7 +111,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('/home'), onPressed: () => Navigator.of(context).pushNamedAndRemoveUntil('/home', (Route<dynamic> route) => false),
), ),
] ]
) )

View File

@ -4,47 +4,29 @@ import 'package:provider/provider.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart'; import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'dart:io'; import 'dart:io';
import 'api.dart'; import 'api.dart';
import 'util.dart'; import 'util.dart';
import 'model.dart'; import 'object.dart';
import 'lib.dart';
class _ProjectScreenState extends State<ProjectScreen> { class _ProjectScreenState extends State<ProjectScreen> {
final String username; final Function _onUpdate;
final String projectPath; final Function _onDelete;
final String fullPath;
final Function? onUpdate;
final Function? onDelete;
final picker = ImagePicker(); final picker = ImagePicker();
final Api api = Api(); final Api api = Api();
final Util util = Util(); final Util util = Util();
Map<String,dynamic>? project; Map<String,dynamic> _project;
List<dynamic> _objects = []; List<dynamic> _objects = [];
bool _loading = false; bool _loading = false;
bool _creating = false; bool _creating = false;
_ProjectScreenState(this.username, this.projectPath, {this.project, this.onUpdate, this.onDelete}) : _ProjectScreenState(this._project, this._onUpdate, this._onDelete) { }
fullPath = username + '/' + projectPath;
@override @override
initState() { initState() {
super.initState(); super.initState();
getProject(fullPath); getObjects(_project['fullName']);
getObjects(fullPath);
}
void getProject(String fullName) async {
setState(() => _loading = true);
var data = await api.request('GET', '/projects/' + fullName);
if (data['success'] == true) {
setState(() {
project = data['payload'];
_loading = false;
});
}
} }
void getObjects(String fullName) async { void getObjects(String fullName) async {
@ -59,18 +41,18 @@ class _ProjectScreenState extends State<ProjectScreen> {
} }
void _shareProject() { void _shareProject() {
util.shareUrl('Check out my project on Treadl', util.appUrl(fullPath)); util.shareUrl('Check out my project on Treadl', util.appUrl(_project['fullName']));
} }
void _onDeleteProject() { void _onDeleteProject() {
context.pop(); Navigator.pop(context);
onDelete!(project!['_id']); _onDelete(_project['_id']);
} }
void _onUpdateProject(project) { void _onUpdateProject(project) {
setState(() { setState(() {
project = project; _project = project;
}); });
onUpdate!(project!['_id'], project!); _onUpdate(project['_id'], project);
} }
void _onUpdateObject(String id, Map<String,dynamic> update) { void _onUpdateObject(String id, Map<String,dynamic> update) {
@ -92,6 +74,7 @@ class _ProjectScreenState extends State<ProjectScreen> {
} }
void _createObject(objectData) async { void _createObject(objectData) async {
String fullPath = _project['fullName'];
var resp = await api.request('POST', '/projects/$fullPath/objects', objectData); var resp = await api.request('POST', '/projects/$fullPath/objects', objectData);
setState(() => _creating = false); setState(() => _creating = false);
if (resp['success']) { if (resp['success']) {
@ -114,7 +97,7 @@ class _ProjectScreenState extends State<ProjectScreen> {
void _createObjectFromFile(String name, XFile file) async { void _createObjectFromFile(String name, XFile file) async {
final int size = await file.length(); final int size = await file.length();
final String forId = project!['_id']; final String forId = _project['_id'];
final String type = file.mimeType ?? 'text/plain'; final String type = file.mimeType ?? 'text/plain';
setState(() => _creating = true); setState(() => _creating = true);
var data = await api.request('GET', '/uploads/file/request?name=$name&size=$size&type=$type&forType=project&forId=$forId'); var data = await api.request('GET', '/uploads/file/request?name=$name&size=$size&type=$type&forType=project&forId=$forId');
@ -156,7 +139,7 @@ class _ProjectScreenState extends State<ProjectScreen> {
if (imageFile == null) return; if (imageFile == null) return;
final f = new DateFormat('yyyy-MM-dd_hh-mm-ss'); final f = new DateFormat('yyyy-MM-dd_hh-mm-ss');
String time = f.format(new DateTime.now()); String time = f.format(new DateTime.now());
String name = project!['name'] + ' ' + time + '.' + imageFile.name.split('.').last; String name = _project['name'] + ' ' + time + '.' + imageFile.name.split('.').last;
_createObjectFromFile(name, imageFile); _createObjectFromFile(name, imageFile);
} }
on Exception { on Exception {
@ -169,7 +152,7 @@ class _ProjectScreenState extends State<ProjectScreen> {
CupertinoDialogAction( CupertinoDialogAction(
isDefaultAction: true, isDefaultAction: true,
child: Text('OK'), child: Text('OK'),
onPressed: () => context.pop(), onPressed: () => Navigator.pop(context),
), ),
], ],
) )
@ -178,7 +161,7 @@ class _ProjectScreenState extends State<ProjectScreen> {
} }
void showSettingsModal() { void showSettingsModal() {
Widget settingsDialog = new _ProjectSettingsDialog(project!, _onDeleteProject, _onUpdateProject); Widget settingsDialog = new _ProjectSettingsDialog(_project, _onDeleteProject, _onUpdateProject);
showCupertinoModalPopup(context: context, builder: (BuildContext context) => settingsDialog); showCupertinoModalPopup(context: context, builder: (BuildContext context) => settingsDialog);
} }
@ -253,7 +236,12 @@ class _ProjectScreenState extends State<ProjectScreen> {
return new Card( return new Card(
child: InkWell( child: InkWell(
onTap: () { onTap: () {
context.push('/' + username + '/' + projectPath + '/' + object['_id']); Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ObjectScreen(object, _project, _onUpdateObject, _onDeleteObject),
),
);
}, },
child: ListTile( child: ListTile(
leading: leader, leading: leader,
@ -265,27 +253,11 @@ 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 EmptyBox('This project is currently empty', description: 'If this is your project, you can add a pattern file, an image, or something else to this project using the + button below.');
}
@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']),
actions: <Widget>[ actions: <Widget>[
IconButton( IconButton(
icon: Icon(Icons.ios_share), icon: Icon(Icons.ios_share),
@ -293,21 +265,41 @@ class _ProjectScreenState extends State<ProjectScreen> {
_shareProject(); _shareProject();
}, },
), ),
onUpdate != null ? IconButton( IconButton(
icon: Icon(Icons.settings), icon: Icon(Icons.settings),
onPressed: () { onPressed: () {
showSettingsModal(); showSettingsModal();
}, },
) : SizedBox(width: 0), ),
] ]
), ),
body: Container( body: _loading ?
Container(
margin: const EdgeInsets.all(10.0),
alignment: Alignment.center,
child: CircularProgressIndicator()
)
: Container(
margin: const EdgeInsets.all(10.0), margin: const EdgeInsets.all(10.0),
alignment: Alignment.center, child: ((_objects != null && _objects.length > 0) || _creating) ?
child: getBody(), 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: Util.canEditProject(user, project) ? ExpandableFab( floatingActionButton: ExpandableFab(
distance: 70, distance: 70,
type: ExpandableFabType.up, type: ExpandableFabType.up,
openButtonBuilder: RotateFloatingActionButtonBuilder( openButtonBuilder: RotateFloatingActionButtonBuilder(
@ -347,30 +339,26 @@ class _ProjectScreenState extends State<ProjectScreen> {
), ),
]), ]),
], ],
) : null, ),
); );
} }
} }
class ProjectScreen extends StatefulWidget { class ProjectScreen extends StatefulWidget {
final String username; final Map<String,dynamic> _project;
final String projectPath; final Function _onUpdate;
final Map<String,dynamic>? project; final Function _onDelete;
final Function? onUpdate; ProjectScreen(this._project, this._onUpdate, this._onDelete) { }
final Function? onDelete;
ProjectScreen(this.username, this.projectPath, {this.project, this.onUpdate, this.onDelete}) { }
@override @override
_ProjectScreenState createState() => _ProjectScreenState(username, projectPath, project: project, onUpdate: onUpdate, onDelete: onDelete); _ProjectScreenState createState() => _ProjectScreenState(_project, _onUpdate, _onDelete);
} }
class _ProjectSettingsDialog extends StatelessWidget { class _ProjectSettingsDialog extends StatelessWidget {
final String fullPath; final Map<String,dynamic> _project;
final Map<String,dynamic> project;
final Function _onDelete; final Function _onDelete;
final Function _onUpdateProject; final Function _onUpdateProject;
final Api api = Api(); final Api api = Api();
_ProjectSettingsDialog(this.project, this._onDelete, this._onUpdateProject) : _ProjectSettingsDialog(this._project, this._onDelete, this._onUpdateProject) {}
fullPath = project['owner']['username'] + '/' + project['path'];
void _renameProject(BuildContext context) async { void _renameProject(BuildContext context) async {
TextEditingController renameController = TextEditingController(); TextEditingController renameController = TextEditingController();
@ -388,18 +376,18 @@ class _ProjectSettingsDialog extends StatelessWidget {
TextButton( TextButton(
child: Text('CANCEL'), child: Text('CANCEL'),
onPressed: () { onPressed: () {
context.pop(); Navigator.pop(context);
}, },
), ),
TextButton( TextButton(
child: Text('OK'), child: Text('OK'),
onPressed: () async { onPressed: () async {
var data = await api.request('PUT', '/projects/' + fullPath, {'name': renameController.text}); var data = await api.request('PUT', '/projects/' + _project['owner']['username'] + '/' + _project['path'], {'name': renameController.text});
if (data['success']) { if (data['success']) {
context.pop(); Navigator.pop(context);
_onUpdateProject(data['payload']); _onUpdateProject(data['payload']);
} }
context.pop(); Navigator.pop(context);
}, },
), ),
], ],
@ -409,18 +397,18 @@ class _ProjectSettingsDialog extends StatelessWidget {
} }
void _toggleVisibility(BuildContext context, bool checked) async { void _toggleVisibility(BuildContext context, bool checked) async {
var data = await api.request('PUT', '/projects/' + fullPath, {'visibility': checked ? 'private': 'public'}); var data = await api.request('PUT', '/projects/' + _project['owner']['username'] + '/' + _project['path'], {'visibility': checked ? 'private': 'public'});
if (data['success']) { if (data['success']) {
context.pop(); Navigator.pop(context);
_onUpdateProject(data['payload']); _onUpdateProject(data['payload']);
} }
} }
void _deleteProject(BuildContext context, BuildContext modalContext) async { void _deleteProject(BuildContext context, BuildContext modalContext) async {
var data = await api.request('DELETE', '/projects/' + fullPath); var data = await api.request('DELETE', '/projects/' + _project['owner']['username'] + '/' + _project['path']);
if (data['success']) { if (data['success']) {
context.pop(); Navigator.pop(context);
context.pop(); Navigator.pop(modalContext);
_onDelete(); _onDelete();
} }
} }
@ -435,7 +423,7 @@ class _ProjectSettingsDialog extends StatelessWidget {
CupertinoDialogAction( CupertinoDialogAction(
isDefaultAction: true, isDefaultAction: true,
child: Text('No'), child: Text('No'),
onPressed: () => context.pop(), onPressed: () => Navigator.pop(context),
), ),
CupertinoDialogAction( CupertinoDialogAction(
isDestructiveAction: true, isDestructiveAction: true,
@ -452,7 +440,7 @@ class _ProjectSettingsDialog extends StatelessWidget {
return CupertinoActionSheet( return CupertinoActionSheet(
title: Text('Manage this project'), title: Text('Manage this project'),
cancelButton: CupertinoActionSheetAction( cancelButton: CupertinoActionSheetAction(
onPressed: () => context.pop(), onPressed: () => Navigator.of(context).pop(),
child: Text('Cancel') child: Text('Cancel')
), ),
actions: [ actions: [
@ -462,7 +450,7 @@ class _ProjectSettingsDialog extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
CupertinoSwitch( CupertinoSwitch(
value: project?['visibility'] == 'private', value: _project['visibility'] == 'private',
onChanged: (c) => _toggleVisibility(context, c), onChanged: (c) => _toggleVisibility(context, c),
), ),
SizedBox(width: 10), SizedBox(width: 10),

View File

@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart'; import 'routeArguments.dart';
import 'api.dart'; import 'api.dart';
import 'model.dart'; import 'project.dart';
import 'lib.dart'; import 'settings.dart';
class _ProjectsTabState extends State<ProjectsTab> { class _ProjectsTabState extends State<ProjectsTab> {
List<dynamic> _projects = []; List<dynamic> _projects = [];
@ -19,8 +19,6 @@ 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;
}); });
@ -83,7 +81,12 @@ class _ProjectsTabState extends State<ProjectsTab> {
return new Card( return new Card(
child: InkWell( child: InkWell(
onTap: () { onTap: () {
context.push('/' + project['owner']['username'] + '/' + project['path']); Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProjectScreen(project, _onUpdateProject, _onDeleteProject),
),
);
}, },
child: Container( child: Container(
padding: EdgeInsets.all(5), padding: EdgeInsets.all(5),
@ -107,56 +110,56 @@ class _ProjectsTabState extends State<ProjectsTab> {
; ;
} }
Widget getBody() {
AppModel model = Provider.of<AppModel>(context);
if (model.user == null)
return LoginNeeded(text: 'Once logged in, you\'ll find your own projects shown here.');
if (_loading)
return CircularProgressIndicator();
else if (_projects != null && _projects.length > 0)
return ListView.builder(
itemCount: _projects.length,
itemBuilder: (BuildContext context, int index) {
return buildProjectCard(_projects[index]);
},
);
else return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Create your first project', style: TextStyle(fontSize: 20), textAlign: TextAlign.center),
Image(image: AssetImage('assets/reading.png'), width: 300),
Text('Projects contain all the files and patterns that make up a piece of work. Create one 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('My Projects'), title: Text('Your Projects'),
actions: <Widget>[ actions: <Widget>[
IconButton( IconButton(
icon: Icon(Icons.info_outline), icon: Icon(Icons.info_outline),
onPressed: () { onPressed: () {
context.push('/settings'); Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SettingsScreen(),
),
);
}, },
), ),
] ]
), ),
body: Container( body: _loading ?
Container(
margin: const EdgeInsets.all(10.0),
alignment: Alignment.center,
child: CircularProgressIndicator()
)
: Container(
margin: const EdgeInsets.all(10.0), margin: const EdgeInsets.all(10.0),
alignment: Alignment.center, alignment: Alignment.center,
child: getBody() child: (_projects != null && _projects.length > 0) ?
ListView.builder(
itemCount: _projects.length,
itemBuilder: (BuildContext context, int index) {
return buildProjectCard(_projects[index]);
},
)
:
Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Create your first project', style: TextStyle(fontSize: 20), textAlign: TextAlign.center),
Image(image: AssetImage('assets/reading.png'), width: 300),
Text('Projects contain all the files and patterns that make up a piece of work. Create one using the + button below.', textAlign: TextAlign.center),
])
), ),
floatingActionButton: user != null ? FloatingActionButton( floatingActionButton: 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, ),
); );
} }
} }
@ -189,7 +192,7 @@ class _NewProjectDialogState extends State<_NewProjectDialog> {
var data = await api.request('POST', '/projects', {'name': name, 'visibility': priv ? 'private' : 'public'}); var data = await api.request('POST', '/projects', {'name': name, 'visibility': priv ? 'private' : 'public'});
if (data['success'] == true) { if (data['success'] == true) {
_onComplete(data['payload']); _onComplete(data['payload']);
context.pop(); Navigator.of(context).pop();
} }
} }
@ -225,7 +228,7 @@ class _NewProjectDialogState extends State<_NewProjectDialog> {
SizedBox(height: 10), SizedBox(height: 10),
CupertinoButton( CupertinoButton(
onPressed: () { onPressed: () {
context.pop(); Navigator.of(context).pop();
}, },
child: Text('Cancel'), child: Text('Cancel'),
) )

View File

@ -1,11 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart';
import 'api.dart'; import 'api.dart';
import 'model.dart';
class _RegisterScreenState extends State<RegisterScreen> { class _RegisterScreenState extends State<RegisterScreen> {
final TextEditingController _usernameController = TextEditingController(); final TextEditingController _usernameController = TextEditingController();
@ -19,9 +17,10 @@ class _RegisterScreenState extends State<RegisterScreen> {
var data = await api.request('POST', '/accounts/register', {'username': _usernameController.text, 'email': _emailController.text, 'password': _passwordController.text}); var data = await api.request('POST', '/accounts/register', {'username': _usernameController.text, 'email': _emailController.text, 'password': _passwordController.text});
setState(() => _registering = false); setState(() => _registering = false);
if (data['success'] == true) { if (data['success'] == true) {
AppModel model = Provider.of<AppModel>(context, listen: false); String token = data['payload']['token'];
model.setToken(data['payload']['token']); SharedPreferences prefs = await SharedPreferences.getInstance();
context.go('/onboarding'); prefs.setString('apiToken', token);
Navigator.of(context).pushNamedAndRemoveUntil('/onboarding', (Route<dynamic> route) => false);
} }
else { else {
showDialog( showDialog(
@ -33,7 +32,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
CupertinoDialogAction( CupertinoDialogAction(
isDefaultAction: true, isDefaultAction: true,
child: Text('Try again'), child: Text('Try again'),
onPressed: () => context.pop(), onPressed: () => Navigator.pop(context),
), ),
], ],
) )
@ -48,59 +47,66 @@ class _RegisterScreenState extends State<RegisterScreen> {
title: Text('Register with Treadl'), title: Text('Register with Treadl'),
), ),
body: Container( body: Container(
margin: const EdgeInsets.only(top: 40, left: 10, right: 10), margin: const EdgeInsets.all(10.0),
child: ListView( child: SingleChildScrollView(
children: <Widget>[ child:Column(
TextField( mainAxisAlignment: MainAxisAlignment.center,
autofocus: true, children: <Widget>[
controller: _usernameController, Image(image: AssetImage('assets/logo.png'), width: 100),
decoration: InputDecoration( SizedBox(height: 20),
hintText: 'username', labelText: 'Choose a username', Text('Register a free account.', style: TextStyle(fontSize: 18)),
border: OutlineInputBorder(), SizedBox(height: 20),
TextField(
autofocus: true,
controller: _usernameController,
decoration: InputDecoration(
hintText: 'username', labelText: 'Choose a username',
border: OutlineInputBorder(),
),
), ),
), SizedBox(height: 10),
SizedBox(height: 10), TextField(
TextField( controller: _emailController,
controller: _emailController, decoration: InputDecoration(
decoration: InputDecoration( hintText: 'sam@example.com', labelText: 'Your email address', helperText: 'For notifications & password resets - we never share this.',
hintText: 'sam@example.com', labelText: 'Your email address', helperText: 'For notifications & password resets - we never share this.', border: OutlineInputBorder()
border: OutlineInputBorder() ),
), ),
), SizedBox(height: 10),
SizedBox(height: 10), TextField(
TextField( onEditingComplete: () => _submit(context),
onEditingComplete: () => _submit(context), controller: _passwordController,
controller: _passwordController, obscureText: true,
obscureText: true, decoration: InputDecoration(
decoration: InputDecoration( hintText: 'Type your password', labelText: 'Choose a strong password',
hintText: 'Type your password', labelText: 'Choose a strong password', border: OutlineInputBorder()
border: OutlineInputBorder() ),
), ),
), SizedBox(height: 20),
SizedBox(height: 20), RichText(
RichText(
textAlign: TextAlign.center,
text: TextSpan(
text: 'By registering you agree to Treadl\'s ',
style: Theme.of(context).textTheme.bodyText1,
children: <TextSpan>[
TextSpan(text: 'Terms of Use', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.pink), recognizer: new TapGestureRecognizer()..onTap = () => launch('https://treadl.com/terms-of-use')),
TextSpan(text: ' and '),
TextSpan(text: 'Privacy Policy', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.pink), recognizer: new TapGestureRecognizer()..onTap = () => launch('https://treadl.com/privacy')),
TextSpan(text: '.'),
],
),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => _submit(context),
//color: Colors.pink,
child: _registering ? SizedBox(height: 20, width: 20, child:CircularProgressIndicator(backgroundColor: Colors.white)) : Text("Register",
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(color: Colors.white, fontSize: 15) text: TextSpan(
) text: 'By registering you agree to Treadl\'s ',
), style: Theme.of(context).textTheme.bodyText1,
] children: <TextSpan>[
TextSpan(text: 'Terms of Use', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.pink), recognizer: new TapGestureRecognizer()..onTap = () => launch('https://treadl.com/terms-of-use')),
TextSpan(text: ' and '),
TextSpan(text: 'Privacy Policy', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.pink), recognizer: new TapGestureRecognizer()..onTap = () => launch('https://treadl.com/privacy')),
TextSpan(text: '.'),
],
),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => _submit(context),
//color: Colors.pink,
child: _registering ? SizedBox(height: 20, width: 20, child:CircularProgressIndicator(backgroundColor: Colors.white)) : Text("Register",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white, fontSize: 15)
)
),
]
)
) )
), ),
); );

View File

@ -1,21 +1,18 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:go_router/go_router.dart';
import 'api.dart'; import 'api.dart';
import 'model.dart';
class SettingsScreen extends StatelessWidget { class SettingsScreen extends StatelessWidget {
final TextEditingController _passwordController = TextEditingController(); final TextEditingController _passwordController = TextEditingController();
void _logout(BuildContext context) async { void _logout(BuildContext context) async {
AppModel model = Provider.of<AppModel>(context, listen: false);
Api api = Api(); Api api = Api();
api.request('POST', '/accounts/logout'); api.request('POST', '/accounts/logout');
model.setToken(null); SharedPreferences prefs = await SharedPreferences.getInstance();
model.setUser(null); prefs.remove('apiToken');
context.pop(); Navigator.of(context).pushNamedAndRemoveUntil('/welcome', (Route<dynamic> route) => false);
} }
void _deleteAccount(BuildContext context) async { void _deleteAccount(BuildContext context) async {
@ -36,7 +33,7 @@ class SettingsScreen extends StatelessWidget {
actions: [ actions: [
TextButton( TextButton(
child: Text('Cancel'), child: Text('Cancel'),
onPressed: () => context.pop(), onPressed: () { Navigator.of(context).pop(); }
), ),
ElevatedButton( ElevatedButton(
child: Text('Delete Account'), child: Text('Delete Account'),
@ -44,10 +41,9 @@ class SettingsScreen extends StatelessWidget {
Api api = Api(); Api api = Api();
var data = await api.request('DELETE', '/accounts', {'password': _passwordController.text}); var data = await api.request('DELETE', '/accounts', {'password': _passwordController.text});
if (data['success'] == true) { if (data['success'] == true) {
AppModel model = Provider.of<AppModel>(context, listen: false); SharedPreferences prefs = await SharedPreferences.getInstance();
model.setToken(null); prefs.remove('apiToken');
model.setUser(null); Navigator.of(context).pushNamedAndRemoveUntil('/welcome', (Route<dynamic> route) => false);
context.go('/home');
} else { } else {
showDialog( showDialog(
context: context, context: context,
@ -58,7 +54,7 @@ class SettingsScreen extends StatelessWidget {
CupertinoDialogAction( CupertinoDialogAction(
isDefaultAction: true, isDefaultAction: true,
child: Text('OK'), child: Text('OK'),
onPressed: () => context.pop(), onPressed: () => Navigator.pop(context),
), ),
], ],
) )
@ -79,8 +75,6 @@ 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'),
@ -99,23 +93,15 @@ class SettingsScreen extends StatelessWidget {
SizedBox(height: 30), SizedBox(height: 30),
user != null ? Column( ListTile(
children: [ leading: Icon(Icons.exit_to_app),
ListTile( title: Text('Logout'),
leading: Icon(Icons.exit_to_app), onTap: () => _logout(context),
title: Text('Logout'), ),
onTap: () => _logout(context), ListTile(
), leading: Icon(Icons.delete),
ListTile( title: Text('Delete Account'),
leading: Icon(Icons.delete), onTap: () => _deleteAccount(context),
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), SizedBox(height: 30),

View File

@ -5,22 +5,18 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'util.dart'; import 'util.dart';
import 'api.dart'; import 'api.dart';
import 'lib.dart';
class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateMixin { class _UserScreenState extends State<UserScreen> {
final String username;
final Util util = new Util(); final Util util = new Util();
final Api api = Api(); final Api api = Api();
TabController? _tabController; Map<String,dynamic> _user;
Map<String,dynamic>? _user;
bool _loading = false; bool _loading = false;
_UserScreenState(this.username) { } _UserScreenState(this._user) { }
@override @override
initState() { initState() {
super.initState(); super.initState();
_tabController = new TabController(length: 2, vsync: this); getUser(_user['username']);
getUser(username);
} }
void getUser(String username) async { void getUser(String username) async {
@ -35,136 +31,85 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
} }
} }
Widget getBody() {
if (_loading)
return CircularProgressIndicator();
else if (_user != null && _tabController != null) {
var u = _user!;
String? created;
if (u['createdAt'] != null) {
DateTime createdAt = DateTime.parse(u['createdAt']!);
created = DateFormat('MMMM y').format(createdAt);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(children: [
util.avatarImage(util.avatarUrl(u), size: 120),
Container(
padding: EdgeInsets.only(left: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(u['username'], style: Theme.of(context).textTheme.titleLarge),
SizedBox(height: 5),
u['location'] != null ?
Row(children: [
Icon(CupertinoIcons.location),
SizedBox(width: 10),
Text(u['location'])
]) : SizedBox(height: 1),
SizedBox(height: 10),
Text('Member' + (created != null ? (' since ' + created!) : ''),
style: TextStyle(color: Colors.grey[500])
),
SizedBox(height: 10),
u['website'] != null ?
GestureDetector(
onTap: () {
String url = u['website'];
if (!url.startsWith('http')) {
url = 'http://' + url;
}
launch(url);
},
child: Text(u['website'],
style: TextStyle(color: Colors.pink))
) : SizedBox(height: 1),
]
)
)
]),
SizedBox(height: 10),
TabBar(
unselectedLabelColor: Colors.black,
labelColor: Colors.pink,
tabs: [
Tab(
text: 'Profile',
icon: Icon(Icons.person),
),
Tab(
text: 'Projects',
icon: Icon(Icons.folder),
)
],
controller: _tabController!,
indicatorSize: TabBarIndicatorSize.tab,
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 30),
Text(u['bio'] != null ? u['bio'] : 'The user doesn\'t have any more profile information.'),
]
),
(u['projects'] != null && u['projects'].length > 0) ?
Container(
margin: EdgeInsets.only(top: 10),
child: GridView.count(
crossAxisCount: 2,
mainAxisSpacing: 5,
crossAxisSpacing: 5,
childAspectRatio: 1.3,
children: u['projects'].map<Widget>((p) =>
ProjectCard(p)
).toList()
),
) :
Container(
margin: EdgeInsets.all(10),
child: EmptyBox('This user doesn\'t have any public projects'),
),
],
),
)
]);
}
else
return Text('User not found');
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String? created;
if (_user['createdAt'] != null) {
DateTime createdAt = DateTime.parse(_user['createdAt']);
created = DateFormat('MMMM y').format(createdAt);
}
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(username), title: Text(_user['username']),
actions: <Widget>[ actions: <Widget>[
IconButton( IconButton(
icon: Icon(Icons.person), icon: Icon(Icons.person),
onPressed: () { onPressed: () {
launch('https://www.treadl.com/' + username); launch('https://www.treadl.com/' + _user['username']);
}, },
), ),
] ]
), ),
body: Container( body: _loading ?
margin: const EdgeInsets.all(10.0), Container(
alignment: Alignment.center, margin: const EdgeInsets.all(10.0),
child: getBody() alignment: Alignment.center,
child: CircularProgressIndicator()
)
: Container(
padding: EdgeInsets.all(10),
margin: EdgeInsets.only(top: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(children: [
util.avatarImage(util.avatarUrl(_user), size: 120),
Expanded(child: Container(
padding: EdgeInsets.only(left: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(_user['username'], style: Theme.of(context).textTheme.titleLarge),
SizedBox(height: 5),
_user['location'] != null ?
Row(children: [
Icon(CupertinoIcons.location),
SizedBox(width: 10),
Text(_user['location'])
]) : SizedBox(height: 1),
SizedBox(height: 10),
Text('Member' + (created != null ? (' since ' + created!) : ''),
style: TextStyle(color: Colors.grey[500])
),
SizedBox(height: 10),
_user['website'] != null ?
GestureDetector(
onTap: () {
String url = _user['website'];
if (!url.startsWith('http')) {
url = 'http://' + url;
}
launch(url);
},
child: Text(_user['website'],
style: TextStyle(color: Colors.pink))
) : SizedBox(height: 1),
]
)
))
]),
SizedBox(height: 30),
Text(_user['bio'] != null ? _user['bio'] : '')
]
)
), ),
); );
} }
} }
class UserScreen extends StatefulWidget { class UserScreen extends StatefulWidget {
final String username; final Map<String,dynamic> user;
UserScreen(this.username) { } UserScreen(this.user) { }
@override @override
_UserScreenState createState() => _UserScreenState(username); _UserScreenState createState() => _UserScreenState(user);
} }

View File

@ -3,7 +3,7 @@ import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'dart:io'; import 'dart:io';
import 'dart:convert'; import 'dart:convert';
import 'model.dart'; import 'api.dart';
String APP_URL = 'https://www.treadl.com'; String APP_URL = 'https://www.treadl.com';
@ -80,15 +80,4 @@ class Util {
void shareUrl(String text, String url) async { void shareUrl(String text, String url) async {
await Share.share('$text: $url'); await Share.share('$text: $url');
} }
static String ellipsis(String input, int cutoff) {
return (input.length <= cutoff)
? input
: '${input.substring(0, cutoff)}...';
}
static bool canEditProject(User? user, Map<String,dynamic>? project) {
if (user == null || project == null) return false;
return project['user'] == user.id;
}
} }

View File

@ -1,15 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:go_router/go_router.dart';
import 'store.dart'; import 'store.dart';
import 'login.dart'; import 'login.dart';
class WelcomeScreen extends StatelessWidget { class WelcomeScreen extends StatelessWidget {
void _login(BuildContext context) { void _login(BuildContext context) {
context.push('/login'); Navigator.of(context).pushNamed('/login');
} }
void _register(BuildContext context) { void _register(BuildContext context) {
context.push('/register'); Navigator.of(context).pushNamed('/register');
} }
@override @override
@ -37,20 +36,11 @@ class WelcomeScreen extends StatelessWidget {
SizedBox(height: 15), SizedBox(height: 15),
CupertinoButton( CupertinoButton(
onPressed: () => _register(context), onPressed: () => _register(context),
color: Colors.pink[400],
child: new Text("Register", child: new Text("Register",
style: TextStyle(color: Colors.white), style: TextStyle(color: Colors.white),
textAlign: TextAlign.center, textAlign: TextAlign.center,
) )
), ),
SizedBox(height: 35),
CupertinoButton(
onPressed: () => context.pop(),
child: new Text("Cancel",
style: TextStyle(color: Colors.white),
textAlign: TextAlign.center,
)
),
]), ]),
)) ))
); );

View File

@ -264,14 +264,6 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
go_router:
dependency: "direct main"
description:
name: go_router
sha256: "3b40e751eaaa855179b416974d59d29669e750d2e50fcdb2b37f1cb0ca8c803a"
url: "https://pub.dev"
source: hosted
version: "13.0.1"
html: html:
dependency: transitive dependency: transitive
description: description:
@ -392,14 +384,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" version: "1.0.2"
logging:
dependency: transitive
description:
name: logging
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:

View File

@ -37,7 +37,6 @@ dependencies:
path_provider: ^2.1.1 path_provider: ^2.1.1
share_plus: ^7.2.1 share_plus: ^7.2.1
flutter_expandable_fab: ^2.0.0 flutter_expandable_fab: ^2.0.0
go_router: ^13.0.1
#fluttertoast: ^8.0.9 #fluttertoast: ^8.0.9

View File

@ -1,11 +0,0 @@
{
"applinks": {
"apps": [],
"details": [
{
"appID": "38T664W57F.com.treadl",
"paths": ["*"]
}
]
}
}