Compare commits
16 Commits
6e15952ffc
...
a22c2d7d16
Author | SHA1 | Date | |
---|---|---|---|
a22c2d7d16 | |||
0b041b04ad | |||
b1d9a41f9d | |||
33241747cd | |||
12f985b7aa | |||
9eff558ebf | |||
bad485ac1d | |||
bfd828f520 | |||
7647542421 | |||
2b37756567 | |||
8abdf00ef8 | |||
062d5f94e4 | |||
9d2574bcd6 | |||
d4ccd62a34 | |||
522c13cd75 | |||
1179d8859f |
@ -29,11 +29,14 @@ 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):
|
||||||
|
@ -61,8 +61,11 @@ 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})
|
owner = db.users.find_one({'_id': p['user']}, {'username': 1, 'avatar': 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
|
||||||
|
|
||||||
@ -95,7 +98,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, '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, 'type': 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']:
|
||||||
|
@ -30,13 +30,22 @@ 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):
|
||||||
|
BIN
mobile/assets/login.png
Normal file
BIN
mobile/assets/login.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 56 KiB |
@ -48,6 +48,7 @@
|
|||||||
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>"; };
|
||||||
@ -116,6 +117,7 @@
|
|||||||
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 */,
|
||||||
@ -373,6 +375,7 @@
|
|||||||
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;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1520"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@ -12,6 +12,8 @@
|
|||||||
<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>
|
||||||
|
11
mobile/ios/Runner/Runner.entitlements
Normal file
11
mobile/ios/Runner/Runner.entitlements
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?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>
|
@ -4,5 +4,10 @@
|
|||||||
<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>
|
||||||
|
@ -4,5 +4,10 @@
|
|||||||
<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>
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
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) {
|
||||||
|
89
mobile/lib/explore.dart
Normal file
89
mobile/lib/explore.dart
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
|
@ -6,31 +6,41 @@ import 'group_noticeboard.dart';
|
|||||||
import 'group_members.dart';
|
import 'group_members.dart';
|
||||||
|
|
||||||
class _GroupScreenState extends State<GroupScreen> {
|
class _GroupScreenState extends State<GroupScreen> {
|
||||||
|
final String id;
|
||||||
|
Map<String, dynamic>? _group;
|
||||||
int _selectedIndex = 0;
|
int _selectedIndex = 0;
|
||||||
List<Widget> _widgetOptions = <Widget> [];
|
|
||||||
final Map<String, dynamic> _group;
|
|
||||||
|
|
||||||
_GroupScreenState(this._group) {
|
_GroupScreenState(this.id) { }
|
||||||
_widgetOptions = <Widget> [
|
|
||||||
GroupNoticeBoardTab(this._group),
|
@override
|
||||||
GroupMembersTab(this._group)
|
void initState() {
|
||||||
];
|
fetchGroup();
|
||||||
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onItemTapped(int index) {
|
void fetchGroup() async {
|
||||||
setState(() {
|
Api api = Api();
|
||||||
_selectedIndex = index;
|
var data = await api.request('GET', '/groups/' + id);
|
||||||
});
|
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'])
|
title: Text(_group?['name'] ?? 'Group')
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Center(
|
||||||
child: _widgetOptions.elementAt(_selectedIndex),
|
child: _group != null ?
|
||||||
|
[
|
||||||
|
GroupNoticeBoardTab(_group!),
|
||||||
|
GroupMembersTab(_group!)
|
||||||
|
].elementAt(_selectedIndex)
|
||||||
|
: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
bottomNavigationBar: BottomNavigationBar(
|
bottomNavigationBar: BottomNavigationBar(
|
||||||
items: const <BottomNavigationBarItem>[
|
items: const <BottomNavigationBarItem>[
|
||||||
@ -45,15 +55,17 @@ class _GroupScreenState extends State<GroupScreen> {
|
|||||||
],
|
],
|
||||||
currentIndex: _selectedIndex,
|
currentIndex: _selectedIndex,
|
||||||
selectedItemColor: Colors.pink[600],
|
selectedItemColor: Colors.pink[600],
|
||||||
onTap: _onItemTapped,
|
onTap: (int index) => setState(() {
|
||||||
|
_selectedIndex = index;
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GroupScreen extends StatefulWidget {
|
class GroupScreen extends StatefulWidget {
|
||||||
final Map<String,dynamic> group;
|
final String id;
|
||||||
GroupScreen(this.group) { }
|
GroupScreen(this.id) { }
|
||||||
@override
|
@override
|
||||||
_GroupScreenState createState() => _GroupScreenState(group);
|
_GroupScreenState createState() => _GroupScreenState(id);
|
||||||
}
|
}
|
||||||
|
@ -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,14 +33,7 @@ class _GroupMembersTabState extends State<GroupMembersTab> {
|
|||||||
|
|
||||||
Widget getMemberCard(member) {
|
Widget getMemberCard(member) {
|
||||||
return new ListTile(
|
return new ListTile(
|
||||||
onTap: () {
|
onTap: () => context.push('/' + member['username']),
|
||||||
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'])
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
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 = [];
|
||||||
@ -42,8 +38,10 @@ 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': _newEntryController.text});
|
var data = await api.request('POST', '/groups/' + _group['_id'] + '/entries', {'content': 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());
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'dart:convert';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:go_router/go_router.dart';
|
||||||
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,6 +16,8 @@ class _GroupsTabState extends State<GroupsTab> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void getGroups() async {
|
void getGroups() async {
|
||||||
|
AppModel model = Provider.of<AppModel>(context, listen: false);
|
||||||
|
if (model.user == null) return;
|
||||||
setState(() => _loading = true);
|
setState(() => _loading = true);
|
||||||
Api api = Api();
|
Api api = Api();
|
||||||
var data = await api.request('GET', '/groups');
|
var data = await api.request('GET', '/groups');
|
||||||
@ -36,14 +38,7 @@ class _GroupsTabState extends State<GroupsTab> {
|
|||||||
}
|
}
|
||||||
return Card(
|
return Card(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () => context.push('/groups/' + group['_id']),
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => GroupScreen(group),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Icon(Icons.people),
|
leading: Icon(Icons.people),
|
||||||
trailing: Icon(Icons.keyboard_arrow_right),
|
trailing: Icon(Icons.keyboard_arrow_right),
|
||||||
@ -55,38 +50,42 @@ 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('Groups'),
|
title: Text('My Groups'),
|
||||||
),
|
),
|
||||||
body: _loading ?
|
body: Container(
|
||||||
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),
|
||||||
child: (_groups != null && _groups.length > 0) ?
|
alignment: Alignment.center,
|
||||||
ListView.builder(
|
child: getBody()
|
||||||
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),
|
|
||||||
])
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ 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';
|
||||||
|
|
||||||
@ -13,6 +14,7 @@ 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()
|
||||||
];
|
];
|
||||||
@ -31,13 +33,17 @@ 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: 'Projects',
|
label: 'My Projects',
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: Icon(Icons.person),
|
icon: Icon(Icons.person),
|
||||||
label: 'Groups',
|
label: 'My Groups',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
currentIndex: _selectedIndex,
|
currentIndex: _selectedIndex,
|
||||||
|
@ -4,9 +4,12 @@ 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;
|
||||||
@ -100,7 +103,7 @@ class _NoticeboardPostState extends State<NoticeboardPost> {
|
|||||||
if (onDelete != null) {
|
if (onDelete != null) {
|
||||||
onDelete!(_entry);
|
onDelete!(_entry);
|
||||||
}
|
}
|
||||||
Navigator.of(context).pop();
|
context.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,7 +149,7 @@ class _NoticeboardPostState extends State<NoticeboardPost> {
|
|||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
context.pop();
|
||||||
},
|
},
|
||||||
child: Text('Cancel'),
|
child: Text('Cancel'),
|
||||||
)
|
)
|
||||||
@ -165,11 +168,7 @@ class _NoticeboardPostState extends State<NoticeboardPost> {
|
|||||||
Row(
|
Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () => context.push('/' + _entry['authorUser']['username']),
|
||||||
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),
|
||||||
@ -229,3 +228,172 @@ 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),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
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();
|
||||||
@ -11,15 +13,14 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
final Api api = Api();
|
final Api api = Api();
|
||||||
bool _loggingIn = false;
|
bool _loggingIn = false;
|
||||||
|
|
||||||
void _submit(context) async {
|
void _submit(BuildContext 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) {
|
||||||
String token = data['payload']['token'];
|
AppModel model = Provider.of<AppModel>(context, listen: false);
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
await model.setToken(data['payload']['token']);
|
||||||
prefs.setString('apiToken', token);
|
context.go('/onboarding');
|
||||||
Navigator.of(context).pushNamedAndRemoveUntil('/onboarding', (Route<dynamic> route) => false);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
showDialog(
|
showDialog(
|
||||||
@ -31,7 +32,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
CupertinoDialogAction(
|
CupertinoDialogAction(
|
||||||
isDefaultAction: true,
|
isDefaultAction: true,
|
||||||
child: Text('Try again'),
|
child: Text('Try again'),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => context.pop(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -46,19 +47,17 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
title: Text('Login to Treadl'),
|
title: Text('Login to Treadl'),
|
||||||
),
|
),
|
||||||
body: Container(
|
body: Container(
|
||||||
margin: const EdgeInsets.all(10.0),
|
margin: const EdgeInsets.only(top: 40, left: 10, right: 10),
|
||||||
child: Column(
|
child: ListView(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Image(image: AssetImage('assets/logo.png'), width: 100),
|
Text('Login with your Treadl account', style: TextStyle(fontSize: 20)),
|
||||||
SizedBox(height: 20),
|
SizedBox(height: 30),
|
||||||
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),
|
||||||
@ -67,7 +66,8 @@ 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),
|
||||||
|
@ -3,19 +3,54 @@ 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 'store.dart';
|
import 'model.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) => Store(),
|
create: (context) => AppModel(),
|
||||||
child: MyApp()
|
child: MyApp()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -37,21 +72,19 @@ class _AppState extends State<MyApp> {
|
|||||||
// Initialize FlutterFire:
|
// Initialize FlutterFire:
|
||||||
future: _initialization,
|
future: _initialization,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
return MaterialApp(
|
return MaterialApp.router(
|
||||||
|
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(),
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -88,8 +121,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) {
|
||||||
Provider.of<Store>(context, listen: false).setToken(token!);
|
AppModel model = Provider.of<AppModel>(context, listen: false);
|
||||||
|
await model.setToken(token!);
|
||||||
FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;
|
FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;
|
||||||
await _firebaseMessaging.requestPermission(
|
await _firebaseMessaging.requestPermission(
|
||||||
alert: true,
|
alert: true,
|
||||||
@ -106,11 +139,8 @@ 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
|
||||||
|
65
mobile/lib/model.dart
Normal file
65
mobile/lib/model.dart
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
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();
|
||||||
|
}*/
|
||||||
|
}
|
@ -3,37 +3,38 @@ 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 Map<String,dynamic> _project;
|
final String username;
|
||||||
Map<String,dynamic> _object;
|
final String projectPath;
|
||||||
Map<String,dynamic>? _pattern;
|
final String id;
|
||||||
|
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._object, this._project, this._onUpdate, this._onDelete) { }
|
_ObjectScreenState(this.username, this.projectPath, this.id) { }
|
||||||
|
|
||||||
@override
|
@override
|
||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (_object['type'] == 'pattern') {
|
fetchObject();
|
||||||
_fetchPattern();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _fetchPattern() async {
|
void fetchObject() async {
|
||||||
var data = await api.request('GET', '/objects/' + _object['_id']);
|
var data = await api.request('GET', '/objects/' + id);
|
||||||
if (data['success'] == true) {
|
if (data['success'] == true) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_pattern = data['payload']['pattern'];
|
object = data['payload'];
|
||||||
|
pattern = data['payload']['pattern'];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -41,14 +42,14 @@ class _ObjectScreenState extends State<ObjectScreen> {
|
|||||||
void _shareObject() async {
|
void _shareObject() async {
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
File? file;
|
File? file;
|
||||||
if (_object['type'] == 'pattern') {
|
if (object!['type'] == 'pattern') {
|
||||||
var data = await api.request('GET', '/objects/' + _object['_id'] + '/wif');
|
var data = await api.request('GET', '/objects/' + id + '/wif');
|
||||||
if (data['success'] == true) {
|
if (data['success'] == true) {
|
||||||
file = await util.writeFile(_object['name'] + '.wif', data['payload']['wif']);
|
file = await util.writeFile(object!['name'] + '.wif', data['payload']['wif']);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
String fileName = Uri.file(_object['url']).pathSegments.last;
|
String fileName = Uri.file(object!['url']).pathSegments.last;
|
||||||
file = await api.downloadFile(_object['url'], fileName);
|
file = await api.downloadFile(object!['url'], fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
@ -58,12 +59,9 @@ class _ObjectScreenState extends State<ObjectScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _deleteObject(BuildContext context, BuildContext modalContext) async {
|
void _deleteObject(BuildContext context, BuildContext modalContext) async {
|
||||||
var data = await api.request('DELETE', '/objects/' + _object['_id']);
|
var data = await api.request('DELETE', '/objects/' + id);
|
||||||
if (data['success']) {
|
if (data['success']) {
|
||||||
Navigator.pop(context);
|
context.go('/home');
|
||||||
Navigator.pop(modalContext);
|
|
||||||
Navigator.pop(context);
|
|
||||||
_onDelete(_object['_id']);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +75,7 @@ class _ObjectScreenState extends State<ObjectScreen> {
|
|||||||
CupertinoDialogAction(
|
CupertinoDialogAction(
|
||||||
isDefaultAction: true,
|
isDefaultAction: true,
|
||||||
child: Text('No'),
|
child: Text('No'),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => context.pop(),
|
||||||
),
|
),
|
||||||
CupertinoDialogAction(
|
CupertinoDialogAction(
|
||||||
isDestructiveAction: true,
|
isDestructiveAction: true,
|
||||||
@ -105,22 +103,21 @@ class _ObjectScreenState extends State<ObjectScreen> {
|
|||||||
TextButton(
|
TextButton(
|
||||||
child: Text('CANCEL'),
|
child: Text('CANCEL'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
child: Text('OK'),
|
child: Text('OK'),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
var data = await api.request('PUT', '/objects/' + _object['_id'], {'name': renameController.text});
|
var data = await api.request('PUT', '/objects/' + id, {'name': renameController.text});
|
||||||
if (data['success']) {
|
if (data['success']) {
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
_object['name'] = data['payload']['name'];
|
object!['name'] = data['payload']['name'];
|
||||||
_onUpdate(_object['_id'], data['payload']);
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_object = _object;
|
object = object;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -136,7 +133,7 @@ class _ObjectScreenState extends State<ObjectScreen> {
|
|||||||
return CupertinoActionSheet(
|
return CupertinoActionSheet(
|
||||||
title: Text('Manage this object'),
|
title: Text('Manage this object'),
|
||||||
cancelButton: CupertinoActionSheetAction(
|
cancelButton: CupertinoActionSheetAction(
|
||||||
onPressed: () => Navigator.of(modalContext).pop(),
|
onPressed: () => modalContext.pop(),
|
||||||
child: Text('Cancel')
|
child: Text('Cancel')
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@ -156,15 +153,22 @@ class _ObjectScreenState extends State<ObjectScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget getObjectWidget() {
|
Widget getObjectWidget() {
|
||||||
if (_object['isImage'] == true) {
|
if (object == null) {
|
||||||
return Image.network(_object['url']);
|
return Center(child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [CircularProgressIndicator()]
|
||||||
|
));
|
||||||
}
|
}
|
||||||
else if (_object['type'] == 'pattern') {
|
else if (object!['isImage'] == true && object!['url'] != null) {
|
||||||
if (_pattern != null) {
|
print(object!['url']);
|
||||||
return PatternViewer(_pattern!, withEditor: true);
|
return Image.network(object!['url']);
|
||||||
|
}
|
||||||
|
else if (object!['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(
|
||||||
@ -184,7 +188,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']);
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
));
|
));
|
||||||
@ -193,12 +197,14 @@ 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']),
|
title: Text(object?['name'] ?? 'Object'),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.ios_share),
|
icon: Icon(Icons.ios_share),
|
||||||
@ -206,12 +212,12 @@ class _ObjectScreenState extends State<ObjectScreen> {
|
|||||||
_shareObject();
|
_shareObject();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
IconButton(
|
Util.canEditProject(user, object?['projectObject']) ? IconButton(
|
||||||
icon: Icon(Icons.settings),
|
icon: Icon(Icons.settings),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_showSettingsModal(context);
|
_showSettingsModal(context);
|
||||||
},
|
},
|
||||||
),
|
) : SizedBox(height: 0),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
body: Container(
|
body: Container(
|
||||||
@ -228,12 +234,11 @@ class _ObjectScreenState extends State<ObjectScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ObjectScreen extends StatefulWidget {
|
class ObjectScreen extends StatefulWidget {
|
||||||
final Map<String,dynamic> _object;
|
final String username;
|
||||||
final Map<String,dynamic> _project;
|
final String projectPath;
|
||||||
final Function _onUpdate;
|
final String id;
|
||||||
final Function _onDelete;
|
ObjectScreen(this.username, this.projectPath, this.id, ) { }
|
||||||
ObjectScreen(this._object, this._project, this._onUpdate, this._onDelete) { }
|
|
||||||
@override
|
@override
|
||||||
_ObjectScreenState createState() => _ObjectScreenState(_object, _project, _onUpdate, _onDelete);
|
_ObjectScreenState createState() => _ObjectScreenState(username, projectPath, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ 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> {
|
||||||
@ -111,7 +112,7 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
|
|||||||
CupertinoButton(
|
CupertinoButton(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
child: Text('Get started', style: TextStyle(color: Colors.pink)),
|
child: Text('Get started', style: TextStyle(color: Colors.pink)),
|
||||||
onPressed: () => Navigator.of(context).pushNamedAndRemoveUntil('/home', (Route<dynamic> route) => false),
|
onPressed: () => context.go('/home'),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -4,29 +4,47 @@ 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 'object.dart';
|
import 'model.dart';
|
||||||
|
import 'lib.dart';
|
||||||
|
|
||||||
class _ProjectScreenState extends State<ProjectScreen> {
|
class _ProjectScreenState extends State<ProjectScreen> {
|
||||||
final Function _onUpdate;
|
final String username;
|
||||||
final Function _onDelete;
|
final String projectPath;
|
||||||
|
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._project, this._onUpdate, this._onDelete) { }
|
_ProjectScreenState(this.username, this.projectPath, {this.project, this.onUpdate, this.onDelete}) :
|
||||||
|
fullPath = username + '/' + projectPath;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
getObjects(_project['fullName']);
|
getProject(fullPath);
|
||||||
|
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 {
|
||||||
@ -41,18 +59,18 @@ class _ProjectScreenState extends State<ProjectScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _shareProject() {
|
void _shareProject() {
|
||||||
util.shareUrl('Check out my project on Treadl', util.appUrl(_project['fullName']));
|
util.shareUrl('Check out my project on Treadl', util.appUrl(fullPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onDeleteProject() {
|
void _onDeleteProject() {
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
_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) {
|
||||||
@ -74,7 +92,6 @@ 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']) {
|
||||||
@ -97,7 +114,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');
|
||||||
@ -139,7 +156,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 {
|
||||||
@ -152,7 +169,7 @@ class _ProjectScreenState extends State<ProjectScreen> {
|
|||||||
CupertinoDialogAction(
|
CupertinoDialogAction(
|
||||||
isDefaultAction: true,
|
isDefaultAction: true,
|
||||||
child: Text('OK'),
|
child: Text('OK'),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => context.pop(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -161,7 +178,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,12 +253,7 @@ class _ProjectScreenState extends State<ProjectScreen> {
|
|||||||
return new Card(
|
return new Card(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(
|
context.push('/' + username + '/' + projectPath + '/' + object['_id']);
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => ObjectScreen(object, _project, _onUpdateObject, _onDeleteObject),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: leader,
|
leading: leader,
|
||||||
@ -253,11 +265,27 @@ 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']),
|
title: Text(project?['name'] ?? 'Project'),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.ios_share),
|
icon: Icon(Icons.ios_share),
|
||||||
@ -265,41 +293,21 @@ class _ProjectScreenState extends State<ProjectScreen> {
|
|||||||
_shareProject();
|
_shareProject();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
IconButton(
|
onUpdate != null ? IconButton(
|
||||||
icon: Icon(Icons.settings),
|
icon: Icon(Icons.settings),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showSettingsModal();
|
showSettingsModal();
|
||||||
},
|
},
|
||||||
),
|
) : SizedBox(width: 0),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
body: _loading ?
|
body: Container(
|
||||||
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),
|
||||||
child: ((_objects != null && _objects.length > 0) || _creating) ?
|
alignment: Alignment.center,
|
||||||
ListView.builder(
|
child: getBody(),
|
||||||
itemCount: _objects.length + (_creating ? 1 : 0),
|
|
||||||
itemBuilder: (BuildContext context, int index) {
|
|
||||||
return getObjectCard(index);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
:
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text('This project is currently empty', style: TextStyle(fontSize: 20), textAlign: TextAlign.center),
|
|
||||||
Image(image: AssetImage('assets/empty.png'), width: 300),
|
|
||||||
Text('Add a pattern file, an image, or something else to this project using the + button below.', textAlign: TextAlign.center),
|
|
||||||
])
|
|
||||||
),
|
),
|
||||||
floatingActionButtonLocation: ExpandableFab.location,
|
floatingActionButtonLocation: ExpandableFab.location,
|
||||||
floatingActionButton: ExpandableFab(
|
floatingActionButton: Util.canEditProject(user, project) ? ExpandableFab(
|
||||||
distance: 70,
|
distance: 70,
|
||||||
type: ExpandableFabType.up,
|
type: ExpandableFabType.up,
|
||||||
openButtonBuilder: RotateFloatingActionButtonBuilder(
|
openButtonBuilder: RotateFloatingActionButtonBuilder(
|
||||||
@ -339,26 +347,30 @@ class _ProjectScreenState extends State<ProjectScreen> {
|
|||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
),
|
) : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProjectScreen extends StatefulWidget {
|
class ProjectScreen extends StatefulWidget {
|
||||||
final Map<String,dynamic> _project;
|
final String username;
|
||||||
final Function _onUpdate;
|
final String projectPath;
|
||||||
final Function _onDelete;
|
final Map<String,dynamic>? project;
|
||||||
ProjectScreen(this._project, this._onUpdate, this._onDelete) { }
|
final Function? onUpdate;
|
||||||
|
final Function? onDelete;
|
||||||
|
ProjectScreen(this.username, this.projectPath, {this.project, this.onUpdate, this.onDelete}) { }
|
||||||
@override
|
@override
|
||||||
_ProjectScreenState createState() => _ProjectScreenState(_project, _onUpdate, _onDelete);
|
_ProjectScreenState createState() => _ProjectScreenState(username, projectPath, project: project, onUpdate: onUpdate, onDelete: onDelete);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ProjectSettingsDialog extends StatelessWidget {
|
class _ProjectSettingsDialog extends StatelessWidget {
|
||||||
final Map<String,dynamic> _project;
|
final String fullPath;
|
||||||
|
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();
|
||||||
@ -376,18 +388,18 @@ class _ProjectSettingsDialog extends StatelessWidget {
|
|||||||
TextButton(
|
TextButton(
|
||||||
child: Text('CANCEL'),
|
child: Text('CANCEL'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
child: Text('OK'),
|
child: Text('OK'),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
var data = await api.request('PUT', '/projects/' + _project['owner']['username'] + '/' + _project['path'], {'name': renameController.text});
|
var data = await api.request('PUT', '/projects/' + fullPath, {'name': renameController.text});
|
||||||
if (data['success']) {
|
if (data['success']) {
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
_onUpdateProject(data['payload']);
|
_onUpdateProject(data['payload']);
|
||||||
}
|
}
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -397,18 +409,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/' + _project['owner']['username'] + '/' + _project['path'], {'visibility': checked ? 'private': 'public'});
|
var data = await api.request('PUT', '/projects/' + fullPath, {'visibility': checked ? 'private': 'public'});
|
||||||
if (data['success']) {
|
if (data['success']) {
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
_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/' + _project['owner']['username'] + '/' + _project['path']);
|
var data = await api.request('DELETE', '/projects/' + fullPath);
|
||||||
if (data['success']) {
|
if (data['success']) {
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
Navigator.pop(modalContext);
|
context.pop();
|
||||||
_onDelete();
|
_onDelete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -423,7 +435,7 @@ class _ProjectSettingsDialog extends StatelessWidget {
|
|||||||
CupertinoDialogAction(
|
CupertinoDialogAction(
|
||||||
isDefaultAction: true,
|
isDefaultAction: true,
|
||||||
child: Text('No'),
|
child: Text('No'),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => context.pop(),
|
||||||
),
|
),
|
||||||
CupertinoDialogAction(
|
CupertinoDialogAction(
|
||||||
isDestructiveAction: true,
|
isDestructiveAction: true,
|
||||||
@ -440,7 +452,7 @@ class _ProjectSettingsDialog extends StatelessWidget {
|
|||||||
return CupertinoActionSheet(
|
return CupertinoActionSheet(
|
||||||
title: Text('Manage this project'),
|
title: Text('Manage this project'),
|
||||||
cancelButton: CupertinoActionSheetAction(
|
cancelButton: CupertinoActionSheetAction(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => context.pop(),
|
||||||
child: Text('Cancel')
|
child: Text('Cancel')
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@ -450,7 +462,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),
|
||||||
|
@ -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 'routeArguments.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'api.dart';
|
import 'api.dart';
|
||||||
import 'project.dart';
|
import 'model.dart';
|
||||||
import 'settings.dart';
|
import 'lib.dart';
|
||||||
|
|
||||||
class _ProjectsTabState extends State<ProjectsTab> {
|
class _ProjectsTabState extends State<ProjectsTab> {
|
||||||
List<dynamic> _projects = [];
|
List<dynamic> _projects = [];
|
||||||
@ -19,6 +19,8 @@ class _ProjectsTabState extends State<ProjectsTab> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void getProjects() async {
|
void getProjects() async {
|
||||||
|
AppModel model = Provider.of<AppModel>(context, listen: false);
|
||||||
|
if (model.user == null) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_loading = true;
|
_loading = true;
|
||||||
});
|
});
|
||||||
@ -81,12 +83,7 @@ class _ProjectsTabState extends State<ProjectsTab> {
|
|||||||
return new Card(
|
return new Card(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(
|
context.push('/' + project['owner']['username'] + '/' + project['path']);
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => ProjectScreen(project, _onUpdateProject, _onDeleteProject),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.all(5),
|
padding: EdgeInsets.all(5),
|
||||||
@ -110,56 +107,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('Your Projects'),
|
title: Text('My Projects'),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.info_outline),
|
icon: Icon(Icons.info_outline),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.push(
|
context.push('/settings');
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => SettingsScreen(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
body: _loading ?
|
body: Container(
|
||||||
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: (_projects != null && _projects.length > 0) ?
|
child: getBody()
|
||||||
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: FloatingActionButton(
|
floatingActionButton: user != null ? FloatingActionButton(
|
||||||
onPressed: showNewProjectDialog,
|
onPressed: showNewProjectDialog,
|
||||||
child: _creatingProject ? CircularProgressIndicator(backgroundColor: Colors.white) : Icon(Icons.add),
|
child: _creatingProject ? CircularProgressIndicator(backgroundColor: Colors.white) : Icon(Icons.add),
|
||||||
backgroundColor: Colors.pink[500],
|
backgroundColor: Colors.pink[500],
|
||||||
),
|
) : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -192,7 +189,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']);
|
||||||
Navigator.of(context).pop();
|
context.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,7 +225,7 @@ class _NewProjectDialogState extends State<_NewProjectDialog> {
|
|||||||
SizedBox(height: 10),
|
SizedBox(height: 10),
|
||||||
CupertinoButton(
|
CupertinoButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
context.pop();
|
||||||
},
|
},
|
||||||
child: Text('Cancel'),
|
child: Text('Cancel'),
|
||||||
)
|
)
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
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();
|
||||||
@ -17,10 +19,9 @@ 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) {
|
||||||
String token = data['payload']['token'];
|
AppModel model = Provider.of<AppModel>(context, listen: false);
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
model.setToken(data['payload']['token']);
|
||||||
prefs.setString('apiToken', token);
|
context.go('/onboarding');
|
||||||
Navigator.of(context).pushNamedAndRemoveUntil('/onboarding', (Route<dynamic> route) => false);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
showDialog(
|
showDialog(
|
||||||
@ -32,7 +33,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
CupertinoDialogAction(
|
CupertinoDialogAction(
|
||||||
isDefaultAction: true,
|
isDefaultAction: true,
|
||||||
child: Text('Try again'),
|
child: Text('Try again'),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => context.pop(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -47,66 +48,59 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
title: Text('Register with Treadl'),
|
title: Text('Register with Treadl'),
|
||||||
),
|
),
|
||||||
body: Container(
|
body: Container(
|
||||||
margin: const EdgeInsets.all(10.0),
|
margin: const EdgeInsets.only(top: 40, left: 10, right: 10),
|
||||||
child: SingleChildScrollView(
|
child: ListView(
|
||||||
child:Column(
|
children: <Widget>[
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
TextField(
|
||||||
children: <Widget>[
|
autofocus: true,
|
||||||
Image(image: AssetImage('assets/logo.png'), width: 100),
|
controller: _usernameController,
|
||||||
SizedBox(height: 20),
|
decoration: InputDecoration(
|
||||||
Text('Register a free account.', style: TextStyle(fontSize: 18)),
|
hintText: 'username', labelText: 'Choose a username',
|
||||||
SizedBox(height: 20),
|
border: OutlineInputBorder(),
|
||||||
TextField(
|
|
||||||
autofocus: true,
|
|
||||||
controller: _usernameController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'username', labelText: 'Choose a username',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
SizedBox(height: 10),
|
),
|
||||||
TextField(
|
SizedBox(height: 10),
|
||||||
controller: _emailController,
|
TextField(
|
||||||
decoration: InputDecoration(
|
controller: _emailController,
|
||||||
hintText: 'sam@example.com', labelText: 'Your email address', helperText: 'For notifications & password resets - we never share this.',
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder()
|
hintText: 'sam@example.com', labelText: 'Your email address', helperText: 'For notifications & password resets - we never share this.',
|
||||||
),
|
border: OutlineInputBorder()
|
||||||
),
|
),
|
||||||
SizedBox(height: 10),
|
),
|
||||||
TextField(
|
SizedBox(height: 10),
|
||||||
onEditingComplete: () => _submit(context),
|
TextField(
|
||||||
controller: _passwordController,
|
onEditingComplete: () => _submit(context),
|
||||||
obscureText: true,
|
controller: _passwordController,
|
||||||
decoration: InputDecoration(
|
obscureText: true,
|
||||||
hintText: 'Type your password', labelText: 'Choose a strong password',
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder()
|
hintText: 'Type your password', labelText: 'Choose a strong password',
|
||||||
),
|
border: OutlineInputBorder()
|
||||||
),
|
),
|
||||||
SizedBox(height: 20),
|
),
|
||||||
RichText(
|
SizedBox(height: 20),
|
||||||
|
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,
|
||||||
text: TextSpan(
|
style: TextStyle(color: Colors.white, fontSize: 15)
|
||||||
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)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -1,18 +1,21 @@
|
|||||||
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:shared_preferences/shared_preferences.dart';
|
import 'package:provider/provider.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');
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
model.setToken(null);
|
||||||
prefs.remove('apiToken');
|
model.setUser(null);
|
||||||
Navigator.of(context).pushNamedAndRemoveUntil('/welcome', (Route<dynamic> route) => false);
|
context.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _deleteAccount(BuildContext context) async {
|
void _deleteAccount(BuildContext context) async {
|
||||||
@ -33,7 +36,7 @@ class SettingsScreen extends StatelessWidget {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
child: Text('Cancel'),
|
child: Text('Cancel'),
|
||||||
onPressed: () { Navigator.of(context).pop(); }
|
onPressed: () => context.pop(),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
child: Text('Delete Account'),
|
child: Text('Delete Account'),
|
||||||
@ -41,9 +44,10 @@ 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) {
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
AppModel model = Provider.of<AppModel>(context, listen: false);
|
||||||
prefs.remove('apiToken');
|
model.setToken(null);
|
||||||
Navigator.of(context).pushNamedAndRemoveUntil('/welcome', (Route<dynamic> route) => false);
|
model.setUser(null);
|
||||||
|
context.go('/home');
|
||||||
} else {
|
} else {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -54,7 +58,7 @@ class SettingsScreen extends StatelessWidget {
|
|||||||
CupertinoDialogAction(
|
CupertinoDialogAction(
|
||||||
isDefaultAction: true,
|
isDefaultAction: true,
|
||||||
child: Text('OK'),
|
child: Text('OK'),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => context.pop(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -75,6 +79,8 @@ class SettingsScreen extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
AppModel model = Provider.of<AppModel>(context);
|
||||||
|
User? user = model.user;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('About Treadl'),
|
title: Text('About Treadl'),
|
||||||
@ -93,15 +99,23 @@ class SettingsScreen extends StatelessWidget {
|
|||||||
|
|
||||||
SizedBox(height: 30),
|
SizedBox(height: 30),
|
||||||
|
|
||||||
ListTile(
|
user != null ? Column(
|
||||||
leading: Icon(Icons.exit_to_app),
|
children: [
|
||||||
title: Text('Logout'),
|
ListTile(
|
||||||
onTap: () => _logout(context),
|
leading: Icon(Icons.exit_to_app),
|
||||||
),
|
title: Text('Logout'),
|
||||||
ListTile(
|
onTap: () => _logout(context),
|
||||||
leading: Icon(Icons.delete),
|
),
|
||||||
title: Text('Delete Account'),
|
ListTile(
|
||||||
onTap: () => _deleteAccount(context),
|
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),
|
SizedBox(height: 30),
|
||||||
|
@ -5,18 +5,22 @@ 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> {
|
class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateMixin {
|
||||||
|
final String username;
|
||||||
final Util util = new Util();
|
final Util util = new Util();
|
||||||
final Api api = Api();
|
final Api api = Api();
|
||||||
Map<String,dynamic> _user;
|
TabController? _tabController;
|
||||||
|
Map<String,dynamic>? _user;
|
||||||
bool _loading = false;
|
bool _loading = false;
|
||||||
_UserScreenState(this._user) { }
|
_UserScreenState(this.username) { }
|
||||||
|
|
||||||
@override
|
@override
|
||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
getUser(_user['username']);
|
_tabController = new TabController(length: 2, vsync: this);
|
||||||
|
getUser(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
void getUser(String username) async {
|
void getUser(String username) async {
|
||||||
@ -31,85 +35,136 @@ class _UserScreenState extends State<UserScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(_user['username']),
|
title: Text(username),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.person),
|
icon: Icon(Icons.person),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
launch('https://www.treadl.com/' + _user['username']);
|
launch('https://www.treadl.com/' + username);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
body: _loading ?
|
body: Container(
|
||||||
Container(
|
margin: const EdgeInsets.all(10.0),
|
||||||
margin: const EdgeInsets.all(10.0),
|
alignment: Alignment.center,
|
||||||
alignment: Alignment.center,
|
child: getBody()
|
||||||
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 Map<String,dynamic> user;
|
final String username;
|
||||||
UserScreen(this.user) { }
|
UserScreen(this.username) { }
|
||||||
@override
|
@override
|
||||||
_UserScreenState createState() => _UserScreenState(user);
|
_UserScreenState createState() => _UserScreenState(username);
|
||||||
}
|
}
|
||||||
|
@ -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 'api.dart';
|
import 'model.dart';
|
||||||
|
|
||||||
String APP_URL = 'https://www.treadl.com';
|
String APP_URL = 'https://www.treadl.com';
|
||||||
|
|
||||||
@ -80,4 +80,15 @@ 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
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) {
|
||||||
Navigator.of(context).pushNamed('/login');
|
context.push('/login');
|
||||||
}
|
}
|
||||||
void _register(BuildContext context) {
|
void _register(BuildContext context) {
|
||||||
Navigator.of(context).pushNamed('/register');
|
context.push('/register');
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -36,11 +37,20 @@ 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,
|
||||||
|
)
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
|
@ -264,6 +264,14 @@ 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:
|
||||||
@ -384,6 +392,14 @@ 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:
|
||||||
|
@ -37,6 +37,7 @@ 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
|
||||||
|
|
||||||
|
11
web/public/.well-known/apple-app-site-association
Normal file
11
web/public/.well-known/apple-app-site-association
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"applinks": {
|
||||||
|
"apps": [],
|
||||||
|
"details": [
|
||||||
|
{
|
||||||
|
"appID": "38T664W57F.com.treadl",
|
||||||
|
"paths": ["*"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user