Compare commits

..

11 Commits

39 changed files with 580 additions and 635 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
*.swp *.swp
.DS_Store .DS_Store
mobile/android/app/.cxx/

View File

@ -67,6 +67,7 @@ tasks:
deps: deps:
- lint-web - lint-web
- lint-api - lint-api
- lint-mobile
lint-web: lint-web:
desc: Lint web frontend desc: Lint web frontend
@ -83,6 +84,14 @@ tasks:
- bash -c "source {{.VENV}} && ruff format ." - bash -c "source {{.VENV}} && ruff format ."
- bash -c "source {{.VENV}} && ruff check --fix ." - bash -c "source {{.VENV}} && ruff check --fix ."
lint-mobile:
desc: Lint mobile app
dir: 'mobile'
cmds:
- echo "[MOBILE] Linting Flutter app..."
- dart fix --apply
- dart analyze
clean: clean:
desc: Remove all dependencies desc: Remove all dependencies
cmds: cmds:

View File

@ -1,3 +1,10 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
id "com.google.gms.google-services"
}
def localProperties = new Properties() def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties') def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) { if (localPropertiesFile.exists()) {
@ -6,11 +13,6 @@ if (localPropertiesFile.exists()) {
} }
} }
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode') def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) { if (flutterVersionCode == null) {
flutterVersionCode = '1' flutterVersionCode = '1'
@ -21,10 +23,6 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0' flutterVersionName = '1.0'
} }
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
def keystoreProperties = new Properties() def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties') def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) { if (keystorePropertiesFile.exists()) {
@ -32,20 +30,18 @@ if (keystorePropertiesFile.exists()) {
} }
android { android {
compileSdkVersion 33 compileSdkVersion flutter.compileSdkVersion
namespace 'com.treadl'
sourceSets { sourceSets {
main.java.srcDirs += 'src/main/kotlin' main.java.srcDirs += 'src/main/kotlin'
} }
lintOptions {
disable 'InvalidPackage'
}
defaultConfig { defaultConfig {
applicationId "com.treadl" applicationId "com.treadl"
minSdkVersion 29 minSdk = flutter.minSdkVersion
targetSdkVersion 34 targetSdk = flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
} }
@ -64,14 +60,19 @@ android {
signingConfig signingConfigs.release signingConfig signingConfigs.release
} }
} }
lint {
disable 'InvalidPackage'
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
} }
flutter { flutter {
source '../..' source '../..'
} }
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
apply plugin: 'com.google.gms.google-services'

View File

@ -1,25 +0,0 @@
// Generated file.
//
// If you wish to remove Flutter's multidex support, delete this entire file.
//
// Modifications to this file should be done in a copy under a different name
// as this file may be regenerated.
package io.flutter.app;
import android.app.Application;
import android.content.Context;
import androidx.annotation.CallSuper;
import androidx.multidex.MultiDex;
/**
* Extension of {@link android.app.Application}, adding multidex support.
*/
public class FlutterMultiDexApplication extends Application {
@Override
@CallSuper
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
}

View File

@ -1,17 +1,3 @@
buildscript {
ext.kotlin_version = '1.8.20'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.4.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.gms:google-services:4.3.3'
}
}
allprojects { allprojects {
repositories { repositories {
google() google()

View File

@ -1,5 +1,3 @@
org.gradle.jvmargs=-Xmx1536M org.gradle.jvmargs=-Xmx1536M
android.enableR8=true
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
android.bundle.enableUncompressedNativeLibs=false

View File

@ -1,5 +1,6 @@
#Sun Apr 06 21:07:46 BST 2025
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@ -1,15 +1,26 @@
include ':app' pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
def plugins = new Properties() repositories {
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') google()
if (pluginsFile.exists()) { mavenCentral()
pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } gradlePluginPortal()
}
} }
plugins.each { name, path -> plugins {
def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() id "dev.flutter.flutter-plugin-loader" version "1.0.0"
include ":$name" id "com.android.application" version '8.9.1' apply false
project(":$name").projectDir = pluginDirectory id "org.jetbrains.kotlin.android" version "2.1.10" apply false
id "com.google.gms.google-services" version "4.3.3" apply false
} }
include ":app"

View File

@ -33,31 +33,31 @@ PODS:
- file_picker (0.0.1): - file_picker (0.0.1):
- DKImagePickerController/PhotoGallery - DKImagePickerController/PhotoGallery
- Flutter - Flutter
- Firebase/CoreOnly (11.8.0): - Firebase/CoreOnly (11.10.0):
- FirebaseCore (~> 11.8.0) - FirebaseCore (~> 11.10.0)
- Firebase/Messaging (11.8.0): - Firebase/Messaging (11.10.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 11.8.0) - FirebaseMessaging (~> 11.10.0)
- firebase_core (3.12.1): - firebase_core (3.13.0):
- Firebase/CoreOnly (= 11.8.0) - Firebase/CoreOnly (= 11.10.0)
- Flutter - Flutter
- firebase_messaging (15.2.4): - firebase_messaging (15.2.5):
- Firebase/Messaging (= 11.8.0) - Firebase/Messaging (= 11.10.0)
- firebase_core - firebase_core
- Flutter - Flutter
- FirebaseCore (11.8.1): - FirebaseCore (11.10.0):
- FirebaseCoreInternal (~> 11.8.0) - FirebaseCoreInternal (~> 11.10.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Logger (~> 8.0) - GoogleUtilities/Logger (~> 8.0)
- FirebaseCoreInternal (11.8.0): - FirebaseCoreInternal (11.10.0):
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseInstallations (11.8.0): - FirebaseInstallations (11.10.0):
- FirebaseCore (~> 11.8.0) - FirebaseCore (~> 11.10.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0)
- PromisesObjC (~> 2.4) - PromisesObjC (~> 2.4)
- FirebaseMessaging (11.8.0): - FirebaseMessaging (11.10.0):
- FirebaseCore (~> 11.8.0) - FirebaseCore (~> 11.10.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0) - GoogleDataTransport (~> 10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
@ -172,18 +172,18 @@ SPEC CHECKSUMS:
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2
firebase_core: 8d552814f6c01ccde5d88939fced4ec26f2f5510 firebase_core: 2d4534e7b489907dcede540c835b48981d890943
firebase_messaging: 8b96a4f09841c15a16b96973ef5c3dcfc1a064e4 firebase_messaging: 75bc93a4df25faccad67f6662ae872ac9ae69b64
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d FirebaseCore: 8344daef5e2661eb004b177488d6f9f0f24251b7
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629 FirebaseCoreInternal: ef4505d2afb1d0ebbc33162cb3795382904b5679
FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917 FirebaseInstallations: 9980995bdd06ec8081dfb6ab364162bdd64245c3
FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8 FirebaseMessaging: 2b9f56aa4ed286e1f0ce2ee1d413aabb8f9f5cb9
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
image_picker_ios: afb77645f1e1060a27edb6793996ff9b42256909 image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47

View File

@ -1,12 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'api.dart'; import 'api.dart';
import 'model.dart'; import 'model.dart';
import 'lib.dart';
class _AccountScreenState extends State<AccountScreen> { class _AccountScreenState extends State<AccountScreen> {
final TextEditingController _emailController = TextEditingController(); final TextEditingController _emailController = TextEditingController();
@ -19,8 +16,8 @@ class _AccountScreenState extends State<AccountScreen> {
super.initState(); super.initState();
AppModel model = Provider.of<AppModel>(context, listen: false); AppModel model = Provider.of<AppModel>(context, listen: false);
if (model.user != null) { if (model.user != null) {
_emailController.text = model.user!.email! ?? ''; _emailController.text = model.user!.email ?? '';
_usernameController.text = model.user!.username! ?? ''; _usernameController.text = model.user!.username;
} }
} }
@ -96,7 +93,6 @@ class _AccountScreenState extends State<AccountScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
AppModel model = Provider.of<AppModel>(context); AppModel model = Provider.of<AppModel>(context);
User? user = model.user;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Edit Account'), title: Text('Edit Account'),
@ -149,7 +145,7 @@ class _AccountScreenState extends State<AccountScreen> {
ElevatedButton( ElevatedButton(
child: Text('Open Treadl website'), child: Text('Open Treadl website'),
onPressed: () { onPressed: () {
launch('https://treadl.com/${model.user?.username}'); launchUrl(Uri.parse('https://treadl.com/${model.user?.username}'));
}, },
), ),
] ]
@ -160,6 +156,7 @@ class _AccountScreenState extends State<AccountScreen> {
class AccountScreen extends StatefulWidget { class AccountScreen extends StatefulWidget {
@override @override
AccountScreen(); const AccountScreen({super.key});
_AccountScreenState createState() => _AccountScreenState(); @override
State<AccountScreen> createState() => _AccountScreenState();
} }

View File

@ -1,11 +1,8 @@
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 {
@ -13,7 +10,7 @@ class Api {
final String apiBase = 'https://api.treadl.com'; final String apiBase = 'https://api.treadl.com';
//final String apiBase = 'http://localhost:2001'; //final String apiBase = 'http://localhost:2001';
Api({token: null}) { Api({token}) {
if (token != null) _token = token; if (token != null) _token = token;
} }
@ -29,7 +26,7 @@ class Api {
Map<String,String> headers = {}; Map<String,String> headers = {};
String? token = await loadToken(); String? token = await loadToken();
if (token != null) { if (token != null) {
headers['Authorization'] = 'Bearer ' + token!; headers['Authorization'] = 'Bearer $token';
} }
if (method == 'POST' || method == 'DELETE') { if (method == 'POST' || method == 'DELETE') {
headers['Content-Type'] = 'application/json'; headers['Content-Type'] = 'application/json';
@ -42,17 +39,17 @@ class Api {
return await client.get(url, headers: await getHeaders('GET')); return await client.get(url, headers: await getHeaders('GET'));
} }
Future<http.Response> _post(Uri url, Map<String, dynamic>? data) async { Future<http.Response> _post(Uri url, Map<String, dynamic>? data) async {
String? json = null; String? json;
if (data != null) { if (data != null) {
json = jsonEncode(data!); json = jsonEncode(data);
} }
http.Client client = http.Client(); http.Client client = http.Client();
return await client.post(url, headers: await getHeaders('POST'), body: json); return await client.post(url, headers: await getHeaders('POST'), body: json);
} }
Future<http.Response> _put(Uri url, Map<String, dynamic>? data) async { Future<http.Response> _put(Uri url, Map<String, dynamic>? data) async {
String? json = null; String? json;
if (data != null) { if (data != null) {
json = jsonEncode(data!); json = jsonEncode(data);
} }
http.Client client = http.Client(); http.Client client = http.Client();
return await client.put(url, headers: await getHeaders('POST'), body: json); return await client.put(url, headers: await getHeaders('POST'), body: json);
@ -86,15 +83,13 @@ class Api {
if (response == null) { if (response == null) {
return {'success': false, 'message': 'No response for your request'}; return {'success': false, 'message': 'No response for your request'};
} }
int status = response!.statusCode; int status = response.statusCode;
if (status == 200) { if (status == 200) {
print('SUCCESS'); Map<String, dynamic> respData = jsonDecode(response.body);
Map<String, dynamic> respData = jsonDecode(response!.body);
return {'success': true, 'payload': respData}; return {'success': true, 'payload': respData};
} }
else { else {
print('ERROR'); Map<String, dynamic> respData = jsonDecode(response.body);
Map<String, dynamic> respData = jsonDecode(response!.body);
return {'success': false, 'code': status, 'message': respData['message']}; return {'success': false, 'code': status, 'message': respData['message']};
} }
} }

View File

@ -1,7 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.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 'lib.dart'; import 'lib.dart';
@ -23,7 +20,7 @@ class _ExploreTabState extends State<ExploreTab> {
void getExploreData() async { void getExploreData() async {
if (explorePage == -1) return; if (explorePage == -1) return;
var data = await api.request('GET', '/search/explore?page=${explorePage}'); var data = await api.request('GET', '/search/explore?page=$explorePage');
if (data['success'] == true) { if (data['success'] == true) {
setState(() { setState(() {
loading = false; loading = false;
@ -76,42 +73,42 @@ class _ExploreTabState extends State<ExploreTab> {
alignment: Alignment.center, alignment: Alignment.center,
child: CircularProgressIndicator() child: CircularProgressIndicator()
) )
: Container( : Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.stretch,
crossAxisAlignment: CrossAxisAlignment.stretch, children: [
children: [ SizedBox(height: 10),
SizedBox(height: 10), CustomText('Discover projects', 'h1', margin: 5),
CustomText('Discover projects', 'h1', margin: 5), SizedBox(height: 5),
SizedBox(height: 5), SizedBox(
Container( height: 130,
height: 130, child: ListView(
child: ListView( scrollDirection: Axis.horizontal,
scrollDirection: Axis.horizontal, children: projects.map((p) => ProjectCard(p)).toList()
children: projects.map((p) => ProjectCard(p)).toList() )
) ),
SizedBox(height: 10),
CustomText('Recent patterns', 'h1', margin: 5),
SizedBox(height: 5),
Expanded(child: Container(
margin: EdgeInsets.only(left: 15, right: 15),
child: GridView.count(
crossAxisCount: 2,
mainAxisSpacing: 5,
crossAxisSpacing: 5,
childAspectRatio: 0.9,
children: patternCards,
), ),
SizedBox(height: 10), )),
CustomText('Recent patterns', 'h1', margin: 5), ]
SizedBox(height: 5),
Expanded(child: Container(
margin: EdgeInsets.only(left: 15, right: 15),
child: GridView.count(
crossAxisCount: 2,
mainAxisSpacing: 5,
crossAxisSpacing: 5,
childAspectRatio: 0.9,
children: patternCards,
),
)),
]
)
), ),
); );
} }
} }
class ExploreTab extends StatefulWidget { class ExploreTab extends StatefulWidget {
const ExploreTab({super.key});
@override @override
_ExploreTabState createState() => _ExploreTabState(); State<ExploreTab> createState() => _ExploreTabState();
} }

View File

@ -1,17 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import 'api.dart'; import 'api.dart';
import 'group_noticeboard.dart'; 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; Map<String, dynamic>? _group;
int _selectedIndex = 0; int _selectedIndex = 0;
_GroupScreenState(this.id) { }
@override @override
void initState() { void initState() {
fetchGroup(); fetchGroup();
@ -20,7 +15,7 @@ class _GroupScreenState extends State<GroupScreen> {
void fetchGroup() async { void fetchGroup() async {
Api api = Api(); Api api = Api();
var data = await api.request('GET', '/groups/' + id); var data = await api.request('GET', '/groups/${widget.id}');
if (data['success'] == true) { if (data['success'] == true) {
setState(() { setState(() {
_group = data['payload']; _group = data['payload'];
@ -65,7 +60,7 @@ class _GroupScreenState extends State<GroupScreen> {
class GroupScreen extends StatefulWidget { class GroupScreen extends StatefulWidget {
final String id; final String id;
GroupScreen(this.id) { } const GroupScreen(this.id, {super.key});
@override @override
_GroupScreenState createState() => _GroupScreenState(id); State<GroupScreen> createState() => _GroupScreenState();
} }

View File

@ -1,27 +1,22 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'api.dart'; import 'api.dart';
import 'util.dart'; import 'util.dart';
import 'user.dart';
class _GroupMembersTabState extends State<GroupMembersTab> { class _GroupMembersTabState extends State<GroupMembersTab> {
final Map<String,dynamic> _group;
final Api api = Api(); final Api api = Api();
List<dynamic> _members = []; List<dynamic> _members = [];
bool _loading = false; bool _loading = false;
_GroupMembersTabState(this._group) { }
@override @override
initState() { initState() {
super.initState(); super.initState();
getMembers(_group['_id']); getMembers(widget.group['_id']);
} }
void getMembers(String id) async { void getMembers(String id) async {
setState(() => _loading = true); setState(() => _loading = true);
var data = await api.request('GET', '/groups/' + id + '/members'); var data = await api.request('GET', '/groups/$id/members');
if (data['success'] == true) { if (data['success'] == true) {
setState(() { setState(() {
_members = data['payload']['members']; _members = data['payload']['members'];
@ -31,8 +26,8 @@ class _GroupMembersTabState extends State<GroupMembersTab> {
} }
Widget getMemberCard(member) { Widget getMemberCard(member) {
return new ListTile( return ListTile(
onTap: () => context.push('/' + member['username']), onTap: () => context.push('/${member["username"]}'),
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'])
@ -49,17 +44,15 @@ class _GroupMembersTabState extends State<GroupMembersTab> {
) )
:Column( :Column(
children: [ children: [
Container( Expanded(
child: Expanded( child: ListView.builder(
child: ListView.builder( padding: const EdgeInsets.all(8),
padding: const EdgeInsets.all(8), itemCount: _members.length,
itemCount: _members.length, itemBuilder: (BuildContext context, int index) {
itemBuilder: (BuildContext context, int index) { return getMemberCard(_members[index]);
return getMemberCard(_members[index]); },
},
),
), ),
) ),
] ]
); );
} }
@ -67,7 +60,7 @@ class _GroupMembersTabState extends State<GroupMembersTab> {
class GroupMembersTab extends StatefulWidget { class GroupMembersTab extends StatefulWidget {
final Map<String,dynamic> group; final Map<String,dynamic> group;
GroupMembersTab(this.group) { } const GroupMembersTab(this.group, {super.key});
@override @override
_GroupMembersTabState createState() => _GroupMembersTabState(group); State<GroupMembersTab> createState() => _GroupMembersTabState();
} }

View File

@ -1,34 +1,29 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'api.dart'; import 'api.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 Api api = Api(); final Api api = Api();
Map<String,dynamic> _group;
List<dynamic> _entries = []; List<dynamic> _entries = [];
bool showPostButton = false; bool showPostButton = false;
bool _loading = false; bool _loading = false;
bool _posting = false; bool _posting = false;
_GroupNoticeBoardTabState(this._group) { }
@override @override
initState() { initState() {
super.initState(); super.initState();
getEntries(_group['_id']); getEntries(widget.group['_id']);
_newEntryController.addListener(() { _newEntryController.addListener(() {
setState(() { setState(() {
showPostButton = _newEntryController.text.length > 0 ? true : false; showPostButton = _newEntryController.text.isNotEmpty ? true : false;
}); });
}); });
} }
void getEntries(String id) async { void getEntries(String id) async {
setState(() => _loading = true); setState(() => _loading = true);
var data = await api.request('GET', '/groups/' + id + '/entries'); var data = await api.request('GET', '/groups/$id/entries');
if (data['success'] == true) { if (data['success'] == true) {
setState(() { setState(() {
_entries = data['payload']['entries']; _entries = data['payload']['entries'];
@ -39,9 +34,9 @@ class _GroupNoticeBoardTabState extends State<GroupNoticeBoardTab> {
void _sendPost(context) async { void _sendPost(context) async {
String text = _newEntryController.text; String text = _newEntryController.text;
if (text.length == 0) return; if (text.isEmpty) return;
setState(() => _posting = true); setState(() => _posting = true);
var data = await api.request('POST', '/groups/' + _group['_id'] + '/entries', {'content': text}); var data = await api.request('POST', '/groups/${widget.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());
@ -83,28 +78,26 @@ class _GroupNoticeBoardTabState extends State<GroupNoticeBoardTab> {
return Column( return Column(
children: <Widget>[ children: <Widget>[
NoticeboardInput(_newEntryController, () => _sendPost(context), _posting, label: 'Write a new post to the group'), NoticeboardInput(_newEntryController, () => _sendPost(context), _posting, label: 'Write a new post to the group'),
Container( Expanded(
child: Expanded( child: _loading ?
child: _loading ? Container(
Container( margin: const EdgeInsets.all(10.0),
margin: const EdgeInsets.all(10.0), alignment: Alignment.center,
alignment: Alignment.center, child: CircularProgressIndicator()
child: CircularProgressIndicator() )
) :
: ListView.builder(
ListView.builder( padding: const EdgeInsets.all(0),
padding: const EdgeInsets.all(0), itemCount: entries.length,
itemCount: entries.length, itemBuilder: (BuildContext context, int index) {
itemBuilder: (BuildContext context, int index) { return Container(
return Container( key: Key(entries[index]['_id']),
key: Key(entries[index]['_id']), child: NoticeboardPost(entries[index],
child: NoticeboardPost(entries[index], onDelete: _onDelete,
onDelete: _onDelete, onReply: _onReply,
onReply: _onReply, ));
)); },
}, ),
),
)
) )
] ]
); );
@ -113,7 +106,7 @@ class _GroupNoticeBoardTabState extends State<GroupNoticeBoardTab> {
class GroupNoticeBoardTab extends StatefulWidget { class GroupNoticeBoardTab extends StatefulWidget {
final Map<String,dynamic> group; final Map<String,dynamic> group;
GroupNoticeBoardTab(this.group) { } const GroupNoticeBoardTab(this.group, {super.key});
@override @override
_GroupNoticeBoardTabState createState() => _GroupNoticeBoardTabState(group); State<GroupNoticeBoardTab> createState() => _GroupNoticeBoardTabState();
} }

View File

@ -32,13 +32,13 @@ class _GroupsTabState extends State<GroupsTab> {
Widget buildGroupCard(Map<String,dynamic> group) { Widget buildGroupCard(Map<String,dynamic> group) {
String? description = group['description']; String? description = group['description'];
if (description != null && description.length > 80) { if (description != null && description.length > 80) {
description = description.substring(0, 77) + '...'; description = '${description.substring(0, 77)}...';
} else if (description == null) { } else {
description = 'This group doesn\'t have a description.'; description ??= 'This group doesn\'t have a description.';
} }
return Card( return Card(
child: InkWell( child: InkWell(
onTap: () => context.push('/groups/' + group['_id']), onTap: () => context.push('/groups/${group["_id"]}'),
child: ListTile( child: ListTile(
leading: Icon(Icons.people, size: 40, color: Colors.pink[300]), leading: Icon(Icons.people, size: 40, color: Colors.pink[300]),
trailing: Icon(Icons.keyboard_arrow_right), trailing: Icon(Icons.keyboard_arrow_right),
@ -51,18 +51,18 @@ class _GroupsTabState extends State<GroupsTab> {
Widget getBody() { Widget getBody() {
AppModel model = Provider.of<AppModel>(context); AppModel model = Provider.of<AppModel>(context);
if (model.user == null) if (model.user == null) {
return LoginNeeded(text: 'Once logged in, you\'ll find your groups here.'); return LoginNeeded(text: 'Once logged in, you\'ll find your groups here.');
else if (_loading) } else if (_loading) {
return CircularProgressIndicator(); return CircularProgressIndicator();
else if (_groups != null && _groups.length > 0) } else if (_groups.isNotEmpty) {
return ListView.builder( return ListView.builder(
itemCount: _groups.length, itemCount: _groups.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
return buildGroupCard(_groups[index]); return buildGroupCard(_groups[index]);
}, },
); );
else } else {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@ -72,6 +72,7 @@ class _GroupsTabState extends State<GroupsTab> {
Text('Groups let you meet and keep in touch with others in the weaving community.', textAlign: TextAlign.center), 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), Text('Please use our website to join and leave groups.', textAlign: TextAlign.center),
]); ]);
}
} }
@override @override
@ -90,6 +91,8 @@ class _GroupsTabState extends State<GroupsTab> {
} }
class GroupsTab extends StatefulWidget { class GroupsTab extends StatefulWidget {
const GroupsTab({super.key});
@override @override
_GroupsTabState createState() => _GroupsTabState(); State<GroupsTab> createState() => _GroupsTabState();
} }

View File

@ -1,19 +1,18 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'explore.dart'; import 'explore.dart';
import 'projects.dart'; import 'projects.dart';
import 'groups.dart'; import 'groups.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override @override
State<HomeScreen> createState() => _MyStatefulWidgetState(); State<HomeScreen> createState() => _MyStatefulWidgetState();
} }
class _MyStatefulWidgetState extends State<HomeScreen> { class _MyStatefulWidgetState extends State<HomeScreen> {
int _selectedIndex = 0; int _selectedIndex = 0;
List<Widget> _widgetOptions = <Widget> [ final List<Widget> _widgetOptions = <Widget> [
ExploreTab(), ExploreTab(),
ProjectsTab(), ProjectsTab(),
GroupsTab() GroupsTab()

View File

@ -1,5 +1,4 @@
import 'dart:core'; import 'dart:core';
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart'; 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';
@ -7,9 +6,6 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'api.dart'; import 'api.dart';
import 'util.dart'; import 'util.dart';
import 'user.dart';
import 'object.dart';
import 'project.dart';
class Alert extends StatelessWidget { class Alert extends StatelessWidget {
final String type; final String type;
@ -18,7 +14,7 @@ class Alert extends StatelessWidget {
final String actionText; final String actionText;
final Widget? descriptionWidget; final Widget? descriptionWidget;
final Function? action; final Function? action;
Alert({this.type = 'info', this.title = '', this.description = '', this.descriptionWidget = null, this.actionText = 'Click here', this.action}) {} const Alert({super.key, this.type = 'info', this.title = '', this.description = '', this.descriptionWidget, this.actionText = 'Click here', this.action});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -38,9 +34,9 @@ class Alert extends StatelessWidget {
return Container( return Container(
padding: EdgeInsets.all(15), padding: EdgeInsets.all(15),
margin: EdgeInsets.all(15), margin: EdgeInsets.all(15),
decoration: new BoxDecoration( decoration: BoxDecoration(
color: accentColor, color: accentColor,
borderRadius: new BorderRadius.all(Radius.circular(10.0)), borderRadius: BorderRadius.all(Radius.circular(10.0)),
boxShadow: [ boxShadow: [
BoxShadow(color: Colors.grey[50]!, spreadRadius: 5), BoxShadow(color: Colors.grey[50]!, spreadRadius: 5),
], ],
@ -52,7 +48,7 @@ class Alert extends StatelessWidget {
SizedBox(height: 20), SizedBox(height: 20),
Text(description, textAlign: TextAlign.center), Text(description, textAlign: TextAlign.center),
if (descriptionWidget != null) descriptionWidget!, if (descriptionWidget != null) descriptionWidget!,
if (actionText != null && action != null) if (action != null)
ElevatedButton( ElevatedButton(
child: Text(actionText), child: Text(actionText),
onPressed: () => action!(), onPressed: () => action!(),
@ -67,28 +63,25 @@ class NoticeboardPost extends StatefulWidget {
final Map<String,dynamic> _entry; final Map<String,dynamic> _entry;
final Function? onDelete; final Function? onDelete;
final Function? onReply; final Function? onReply;
NoticeboardPost(this._entry, {this.onDelete = null, this.onReply = null}); const NoticeboardPost(this._entry, {super.key, this.onDelete, this.onReply});
_NoticeboardPostState createState() => _NoticeboardPostState(_entry, onDelete: onDelete, onReply: onReply); @override
State<NoticeboardPost> createState() => _NoticeboardPostState();
} }
class _NoticeboardPostState extends State<NoticeboardPost> { class _NoticeboardPostState extends State<NoticeboardPost> {
final Map<String,dynamic> _entry; final Api api = Api();
final Api api = new Api();
final Function? onDelete;
final Function? onReply;
final TextEditingController _replyController = TextEditingController(); final TextEditingController _replyController = TextEditingController();
bool _isReplying = false; bool _isReplying = false;
bool _replying = false; bool _replying = false;
_NoticeboardPostState(this._entry, {this.onDelete = null, this.onReply = null}) { }
void _sendReply() async { void _sendReply() async {
setState(() => _replying = true); setState(() => _replying = true);
var data = await api.request('POST', '/groups/' + _entry['group'] + '/entries/' + _entry['_id'] + '/replies', {'content': _replyController.text}); var data = await api.request('POST', '/groups/${widget._entry["group"]}/entries/${widget._entry["_id"]}/replies', {'content': _replyController.text});
if (data['success'] == true) { if (data['success'] == true) {
_replyController.value = TextEditingValue(text: ''); _replyController.value = TextEditingValue(text: '');
if (!mounted) return;
FocusScope.of(context).requestFocus(FocusNode()); FocusScope.of(context).requestFocus(FocusNode());
if (onReply != null) { if (widget.onReply != null) {
onReply!(data['payload']); widget.onReply!(data['payload']);
} }
setState(() { setState(() {
_replying = false; _replying = false;
@ -98,38 +91,40 @@ class _NoticeboardPostState extends State<NoticeboardPost> {
} }
void _deletePost() async { void _deletePost() async {
var data = await api.request('DELETE', '/groups/' + _entry['group'] + '/entries/' + _entry['_id']); var data = await api.request('DELETE', '/groups/${widget._entry["group"]}/entries/${widget._entry["_id"]}');
if (data['success'] == true) { if (data['success'] == true) {
if (onDelete != null) { if (widget.onDelete != null) {
onDelete!(_entry); widget.onDelete!(widget._entry);
}
if (mounted) {
context.pop();
} }
context.pop();
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var createdAt = DateTime.parse(_entry['createdAt']); var createdAt = DateTime.parse(widget._entry['createdAt']);
bool isReply = _entry['inReplyTo'] != null; bool isReply = widget._entry['inReplyTo'] != null;
int replyCount = _entry['replies'] == null ? 0 : _entry['replies']!.length; int replyCount = widget._entry['replies'] == null ? 0 : widget._entry['replies']!.length;
String replyText = 'Write a reply...'; String replyText = 'Write a reply...';
if (replyCount == 1) replyText = '1 Reply'; if (replyCount == 1) replyText = '1 Reply';
if (replyCount > 1) replyText = replyCount.toString() + ' replies'; if (replyCount > 1) replyText = '$replyCount replies';
if (_isReplying) replyText = 'Cancel reply'; if (_isReplying) replyText = 'Cancel reply';
List<Widget> replyWidgets = []; List<Widget> replyWidgets = [];
if (_entry['replies'] != null) { if (widget._entry['replies'] != null) {
for (int i = 0; i < _entry['replies']!.length; i++) { for (int i = 0; i < widget._entry['replies']!.length; i++) {
replyWidgets.add(new Container( replyWidgets.add(Container(
key: Key(_entry['replies']![i]['_id']), key: Key(widget._entry['replies']![i]['_id']),
child: NoticeboardPost(_entry['replies']![i], onDelete: onDelete) child: NoticeboardPost(widget._entry['replies']![i], onDelete: widget.onDelete)
)); ));
} }
} }
return new GestureDetector( return GestureDetector(
key: Key(_entry['_id']), key: Key(widget._entry['_id']),
onLongPress: () async { onLongPress: () async {
Dialog simpleDialog = Dialog( Dialog simpleDialog = Dialog(
child: Container( child: SizedBox(
height: 160.0, height: 160.0,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@ -137,7 +132,7 @@ class _NoticeboardPostState extends State<NoticeboardPost> {
ElevatedButton( ElevatedButton(
//color: Colors.orange, //color: Colors.orange,
onPressed: () { onPressed: () {
launch('https://www.treadl.com'); launchUrl(Uri.parse('https://www.treadl.com/report'));
}, },
child: Text('Report this post'), child: Text('Report this post'),
), ),
@ -168,11 +163,11 @@ class _NoticeboardPostState extends State<NoticeboardPost> {
Row( Row(
children: <Widget>[ children: <Widget>[
GestureDetector( GestureDetector(
onTap: () => context.push('/' + _entry['authorUser']['username']), onTap: () => context.push('/${widget._entry["authorUser"]["username"]}'),
child: Util.avatarImage(Util.avatarUrl(_entry['authorUser']), size: isReply ? 30 : 40) child: Util.avatarImage(Util.avatarUrl(widget._entry['authorUser']), size: isReply ? 30 : 40)
), ),
SizedBox(width: 5), SizedBox(width: 5),
Text(_entry['authorUser']['username'], style: TextStyle(color: Colors.pink)), Text(widget._entry['authorUser']['username'], style: TextStyle(color: Colors.pink)),
SizedBox(width: 5), SizedBox(width: 5),
Text(DateFormat('kk:mm on MMMM d y').format(createdAt), style: TextStyle(color: Colors.grey, fontSize: 10)), Text(DateFormat('kk:mm on MMMM d y').format(createdAt), style: TextStyle(color: Colors.grey, fontSize: 10)),
SizedBox(width: 10), SizedBox(width: 10),
@ -184,7 +179,7 @@ class _NoticeboardPostState extends State<NoticeboardPost> {
), ),
Row(children: [ Row(children: [
SizedBox(width: 45), SizedBox(width: 45),
Expanded(child: Text(_entry['content'], textAlign: TextAlign.left)) Expanded(child: Text(widget._entry['content'], textAlign: TextAlign.left))
]), ]),
_isReplying ? NoticeboardInput(_replyController, _sendReply, _replying, label: 'Reply to this post') : SizedBox(width: 0), _isReplying ? NoticeboardInput(_replyController, _sendReply, _replying, label: 'Reply to this post') : SizedBox(width: 0),
Column( Column(
@ -201,7 +196,7 @@ class NoticeboardInput extends StatelessWidget {
final Function _onPost; final Function _onPost;
final bool _posting; final bool _posting;
final String label; final String label;
NoticeboardInput(this._controller, this._onPost, this._posting, {this.label = 'Write a new post'}) {} const NoticeboardInput(this._controller, this._onPost, this._posting, {super.key, this.label = 'Write a new post'});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -218,7 +213,7 @@ class NoticeboardInput extends StatelessWidget {
), ),
)), )),
IconButton( IconButton(
onPressed: () => _onPost!(), onPressed: () => _onPost(),
color: Colors.pink, color: Colors.pink,
icon: _posting ? CircularProgressIndicator() : Icon(Icons.send), icon: _posting ? CircularProgressIndicator() : Icon(Icons.send),
) )
@ -230,13 +225,13 @@ class NoticeboardInput extends StatelessWidget {
class UserChip extends StatelessWidget { class UserChip extends StatelessWidget {
final Map<String,dynamic> user; final Map<String,dynamic> user;
UserChip(this.user) {} const UserChip(this.user, {super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
ImageProvider? avatar = Util.avatarUrl(user); ImageProvider? avatar = Util.avatarUrl(user);
return GestureDetector( return GestureDetector(
onTap: () => context.push('/' + user['username']), onTap: () => context.push('/${user["username"]}'),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
@ -251,7 +246,7 @@ class UserChip extends StatelessWidget {
class PatternCard extends StatelessWidget { class PatternCard extends StatelessWidget {
final Map<String,dynamic> object; final Map<String,dynamic> object;
PatternCard(this.object) {} const PatternCard(this.object, {super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -263,7 +258,7 @@ class PatternCard extends StatelessWidget {
), ),
child: InkWell( child: InkWell(
onTap: () { onTap: () {
context.push('/' + object['projectObject']['owner']['username'] + '/' + object['projectObject']['path'] + '/' + object['_id']); context.push('/${object["projectObject"]["owner"]["username"]}/${object["projectObject"]["path"]}/${object["_id"]}');
}, },
child: Column( child: Column(
children: [ children: [
@ -296,7 +291,7 @@ class PatternCard extends StatelessWidget {
class ProjectCard extends StatelessWidget { class ProjectCard extends StatelessWidget {
final Map<String,dynamic> project; final Map<String,dynamic> project;
ProjectCard(this.project) {} const ProjectCard(this.project, {super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -308,7 +303,7 @@ class ProjectCard extends StatelessWidget {
), ),
child: InkWell( child: InkWell(
onTap: () { onTap: () {
context.push('/' + this.project['owner']['username'] + '/' + this.project['path']); context.push('/${project["owner"]["username"]}/${project["path"]}');
}, },
child: Column( child: Column(
children: [ children: [
@ -337,19 +332,20 @@ class CustomText extends StatelessWidget {
final String text; final String text;
final String type; final String type;
final double margin; final double margin;
TextStyle? style; late final TextStyle style;
CustomText(this.text, this.type, {this.margin = 0}) { } CustomText(this.text, this.type, {super.key, this.margin = 0}) {
if (type == 'h1') {
@override style = TextStyle(fontSize: 30, fontWeight: FontWeight.bold);
Widget build(BuildContext context) {
if (this.type == 'h1') {
style = Theme.of(context).textTheme.titleLarge;
} }
else { else {
style = TextStyle(); style = TextStyle();
} }
}
@override
Widget build(BuildContext context) {
return Container( return Container(
margin: EdgeInsets.all(this.margin), margin: EdgeInsets.all(margin),
child: Text(text, style: style) child: Text(text, style: style)
); );
} }
@ -357,7 +353,7 @@ class CustomText extends StatelessWidget {
class LoginNeeded extends StatelessWidget { class LoginNeeded extends StatelessWidget {
final String? text; final String? text;
LoginNeeded({this.text}) {} const LoginNeeded({super.key, this.text});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
@ -372,7 +368,7 @@ class LoginNeeded extends StatelessWidget {
onPressed: () { onPressed: () {
context.push('/welcome'); context.push('/welcome');
}, },
child: new Text("Login or register", child: Text("Login or register",
textAlign: TextAlign.center, textAlign: TextAlign.center,
) )
) )
@ -385,7 +381,7 @@ class EmptyBox extends StatelessWidget {
final String title; final String title;
final String? description; final String? description;
EmptyBox(this.title, {this.description}) {} const EmptyBox(this.title, {super.key, this.description});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -1,6 +1,5 @@
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:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@ -18,16 +17,19 @@ class _LoginScreenState extends State<LoginScreen> {
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) {
if (!context.mounted) return;
AppModel model = Provider.of<AppModel>(context, listen: false); AppModel model = Provider.of<AppModel>(context, listen: false);
await model.setToken(data['payload']['token']); await model.setToken(data['payload']['token']);
if (!context.mounted) return;
context.go('/onboarding'); context.go('/onboarding');
} }
else { else {
if (!context.mounted) return;
showDialog( showDialog(
context: context, context: context,
builder: (BuildContext context) => CupertinoAlertDialog( builder: (BuildContext context) => CupertinoAlertDialog(
title: new Text("There was a problem logging you in"), title: Text("There was a problem logging you in"),
content: new Text(data['message']), content: Text(data['message']),
actions: <Widget>[ actions: <Widget>[
CupertinoDialogAction( CupertinoDialogAction(
isDefaultAction: true, isDefaultAction: true,
@ -74,7 +76,7 @@ class _LoginScreenState extends State<LoginScreen> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [GestureDetector( children: [GestureDetector(
onTap: () => launch('https://treadl.com/password/forgotten'), onTap: () => launchUrl(Uri.parse('https://treadl.com/password/forgotten')),
child: Text('Forgotten your password?'), child: Text('Forgotten your password?'),
)] )]
), ),
@ -93,6 +95,8 @@ class _LoginScreenState extends State<LoginScreen> {
} }
class LoginScreen extends StatefulWidget { class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override @override
_LoginScreenState createState() => _LoginScreenState(); State<LoginScreen> createState() => _LoginScreenState();
} }

View File

@ -4,7 +4,6 @@ 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:go_router/go_router.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'api.dart'; import 'api.dart';
import 'model.dart'; import 'model.dart';
import 'welcome.dart'; import 'welcome.dart';
@ -15,7 +14,7 @@ import 'home.dart';
import 'project.dart'; import 'project.dart';
import 'object.dart'; import 'object.dart';
import 'settings.dart'; import 'settings.dart';
import 'verifyEmail.dart'; import 'verify_email.dart';
import 'group.dart'; import 'group.dart';
import 'user.dart'; import 'user.dart';
import 'account.dart'; import 'account.dart';
@ -61,9 +60,11 @@ void main() {
} }
class MyApp extends StatefulWidget { class MyApp extends StatefulWidget {
const MyApp({super.key});
// Create the initialization Future outside of `build`: // Create the initialization Future outside of `build`:
@override @override
_AppState createState() => _AppState(); State<MyApp> createState() => _AppState();
} }
class _AppState extends State<MyApp> { class _AppState extends State<MyApp> {
@ -90,17 +91,16 @@ class _AppState extends State<MyApp> {
} }
class Startup extends StatelessWidget { class Startup extends StatelessWidget {
bool _handled = false;
Startup() { Startup({super.key}) {
FirebaseMessaging.onMessage.listen((RemoteMessage message) { /*FirebaseMessaging.onMessage.listen((RemoteMessage message) {
if (message.notification != null) { if (message.notification != null) {
print(message.notification!); print(message.notification!);
String text = ''; String text = '';
if (message.notification != null && message.notification!.body != null) { if (message.notification != null && message.notification!.body != null) {
text = message.notification!.body!; text = message.notification!.body!;
} }
/*Fluttertoast.showToast( Fluttertoast.showToast(
msg: text, msg: text,
toastLength: Toast.LENGTH_LONG, toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.TOP, gravity: ToastGravity.TOP,
@ -108,21 +108,20 @@ class Startup extends StatelessWidget {
backgroundColor: Colors.grey[100], backgroundColor: Colors.grey[100],
textColor: Colors.black, textColor: Colors.black,
fontSize: 16.0 fontSize: 16.0
);*/ );
} }
}); });*/
} }
void checkToken(BuildContext context) async { void checkToken(BuildContext context) async {
if (_handled) return;
_handled = true;
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) {
if (!context.mounted) return;
AppModel model = Provider.of<AppModel>(context, listen: false); AppModel model = Provider.of<AppModel>(context, listen: false);
await model.setToken(token!); await model.setToken(token);
FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance; FirebaseMessaging firebaseMessaging = FirebaseMessaging.instance;
await _firebaseMessaging.requestPermission( await firebaseMessaging.requestPermission(
alert: true, alert: true,
announcement: false, announcement: false,
badge: true, badge: true,
@ -131,13 +130,13 @@ class Startup extends StatelessWidget {
provisional: false, provisional: false,
sound: true, sound: true,
); );
String? _pushToken = await _firebaseMessaging.getToken(); String? pushToken = await firebaseMessaging.getToken();
if (_pushToken != null) { if (pushToken != null) {
print("sending push");
Api api = Api(); Api api = Api();
api.request('PUT', '/accounts/pushToken', {'pushToken': _pushToken!}); api.request('PUT', '/accounts/pushToken', {'pushToken': pushToken});
} }
} }
if (!context.mounted) return;
context.go('/home'); context.go('/home');
} }

View File

@ -10,7 +10,7 @@ class User {
String? avatarUrl; String? avatarUrl;
bool? emailVerified; bool? emailVerified;
User(this.id, this.username, {this.avatar, this.avatarUrl}) {} User(this.id, this.username, {this.avatar, this.avatarUrl});
static User loadJSON(Map<String,dynamic> input) { static User loadJSON(Map<String,dynamic> input) {
User newUser = User(input['_id'], input['username'], avatar: input['avatar'], avatarUrl: input['avatarUrl']); User newUser = User(input['_id'], input['username'], avatar: input['avatar'], avatarUrl: input['avatarUrl']);
@ -58,7 +58,6 @@ class AppModel extends ChangeNotifier {
var data = await api.request('GET', '/users/me'); var data = await api.request('GET', '/users/me');
if (data['success'] == true) { if (data['success'] == true) {
setUser(User.loadJSON(data['payload'])); setUser(User.loadJSON(data['payload']));
print(data);
} }
} else { } else {
prefs.remove('apiToken'); prefs.remove('apiToken');

View File

@ -2,26 +2,19 @@ import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:go_router/go_router.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 'model.dart';
import 'patterns/pattern.dart';
import 'patterns/viewer.dart'; import 'patterns/viewer.dart';
class _ObjectScreenState extends State<ObjectScreen> { class _ObjectScreenState extends State<ObjectScreen> {
final String username;
final String projectPath;
final String id;
Map<String,dynamic>? object; Map<String,dynamic>? object;
Map<String,dynamic>? pattern; Map<String,dynamic>? pattern;
bool _isLoading = false; bool _isLoading = false;
final Api api = Api(); final Api api = Api();
_ObjectScreenState(this.username, this.projectPath, this.id) { }
@override @override
initState() { initState() {
super.initState(); super.initState();
@ -29,7 +22,7 @@ class _ObjectScreenState extends State<ObjectScreen> {
} }
void fetchObject() async { void fetchObject() async {
var data = await api.request('GET', '/objects/' + id); var data = await api.request('GET', '/objects/${widget.id}');
if (data['success'] == true) { if (data['success'] == true) {
setState(() { setState(() {
object = data['payload']; object = data['payload'];
@ -42,7 +35,7 @@ class _ObjectScreenState extends State<ObjectScreen> {
setState(() => _isLoading = true); setState(() => _isLoading = true);
File? file; File? file;
if (object!['type'] == 'pattern') { if (object!['type'] == 'pattern') {
var data = await api.request('GET', '/objects/' + id + '/wif'); var data = await api.request('GET', '/objects/${widget.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']);
} }
@ -53,14 +46,15 @@ class _ObjectScreenState extends State<ObjectScreen> {
} }
if (file != null) { if (file != null) {
Util.shareFile(file!, withDelete: true); Util.shareFile(file, withDelete: true);
} }
setState(() => _isLoading = false); setState(() => _isLoading = false);
} }
void _deleteObject(BuildContext context, BuildContext modalContext) async { void _deleteObject(BuildContext context, BuildContext modalContext) async {
var data = await api.request('DELETE', '/objects/' + id); var data = await api.request('DELETE', '/objects/${widget.id}');
if (data['success']) { if (data['success']) {
if (!context.mounted) return;
context.go('/home'); context.go('/home');
} }
} }
@ -69,8 +63,8 @@ class _ObjectScreenState extends State<ObjectScreen> {
showDialog( showDialog(
context: modalContext, context: modalContext,
builder: (BuildContext context) => CupertinoAlertDialog( builder: (BuildContext context) => CupertinoAlertDialog(
title: new Text('Really delete this item?'), title: Text('Really delete this item?'),
content: new Text('This action cannot be undone.'), content: Text('This action cannot be undone.'),
actions: <Widget>[ actions: <Widget>[
CupertinoDialogAction( CupertinoDialogAction(
isDefaultAction: true, isDefaultAction: true,
@ -109,7 +103,8 @@ class _ObjectScreenState extends State<ObjectScreen> {
TextButton( TextButton(
child: Text('OK'), child: Text('OK'),
onPressed: () async { onPressed: () async {
var data = await api.request('PUT', '/objects/' + id, {'name': renameController.text}); var data = await api.request('PUT', '/objects/${widget.id}', {'name': renameController.text});
if (!context.mounted) return;
if (data['success']) { if (data['success']) {
context.pop(); context.pop();
object!['name'] = data['payload']['name']; object!['name'] = data['payload']['name'];
@ -143,8 +138,8 @@ class _ObjectScreenState extends State<ObjectScreen> {
), ),
CupertinoActionSheetAction( CupertinoActionSheetAction(
onPressed: () => _confirmDeleteObject(modalContext), onPressed: () => _confirmDeleteObject(modalContext),
child: Text('Delete item'),
isDestructiveAction: true, isDestructiveAction: true,
child: Text('Delete item'),
), ),
] ]
); );
@ -160,7 +155,6 @@ class _ObjectScreenState extends State<ObjectScreen> {
)); ));
} }
else if (object!['isImage'] == true && object!['url'] != null) { else if (object!['isImage'] == true && object!['url'] != null) {
print(object!['url']);
return Image.network(object!['url']); return Image.network(object!['url']);
} }
else if (object!['type'] == 'pattern') { else if (object!['type'] == 'pattern') {
@ -168,7 +162,7 @@ class _ObjectScreenState extends State<ObjectScreen> {
return PatternViewer(pattern!, withEditor: true); return PatternViewer(pattern!, withEditor: true);
} }
else if (object!['previewUrl'] != null) { else if (object!['previewUrl'] != null) {
return Image.network(object!['previewUrl']!);; return Image.network(object!['previewUrl']!);
} }
else { else {
return Column( return Column(
@ -188,7 +182,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']); launchUrl(object!['url']);
}), }),
], ],
)); ));
@ -199,9 +193,6 @@ class _ObjectScreenState extends State<ObjectScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
AppModel model = Provider.of<AppModel>(context); AppModel model = Provider.of<AppModel>(context);
User? user = model.user; User? user = model.user;
String description = '';
if (object?['description'] != null)
description = object!['description']!;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(object?['name'] ?? 'Object'), title: Text(object?['name'] ?? 'Object'),
@ -237,8 +228,8 @@ class ObjectScreen extends StatefulWidget {
final String username; final String username;
final String projectPath; final String projectPath;
final String id; final String id;
ObjectScreen(this.username, this.projectPath, this.id, ) { } const ObjectScreen(this.username, this.projectPath, this.id, {super.key});
@override @override
_ObjectScreenState createState() => _ObjectScreenState(username, projectPath, id); State<ObjectScreen> createState() => _ObjectScreenState();
} }

View File

@ -1,6 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.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 'package:go_router/go_router.dart';
import 'api.dart'; import 'api.dart';
@ -22,8 +20,8 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
void _requestPushPermissions() async { void _requestPushPermissions() async {
try { try {
setState(() => _loading = true); setState(() => _loading = true);
FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance; FirebaseMessaging firebaseMessaging = FirebaseMessaging.instance;
await _firebaseMessaging.requestPermission( await firebaseMessaging.requestPermission(
alert: true, alert: true,
announcement: false, announcement: false,
badge: true, badge: true,
@ -32,12 +30,11 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
provisional: false, provisional: false,
sound: true, sound: true,
); );
_pushToken = await _firebaseMessaging.getToken(); _pushToken = await firebaseMessaging.getToken();
if (_pushToken != null) { if (_pushToken != null) {
api.request('PUT', '/accounts/pushToken', {'pushToken': _pushToken!}); api.request('PUT', '/accounts/pushToken', {'pushToken': _pushToken!});
} }
} } catch (_) { }
on Exception { }
setState(() => _loading = false); setState(() => _loading = false);
_controller.animateToPage(2, duration: Duration(milliseconds: 500), curve: Curves.easeInOut); _controller.animateToPage(2, duration: Duration(milliseconds: 500), curve: Curves.easeInOut);
} }
@ -85,12 +82,12 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
Text('We recommend enabling push notifications so you can keep up-to-date with your groups and projects.', style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold), textAlign: TextAlign.center), Text('We recommend enabling push notifications so you can keep up-to-date with your groups and projects.', style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold), textAlign: TextAlign.center),
SizedBox(height: 20), SizedBox(height: 20),
ElevatedButton( ElevatedButton(
onPressed: _requestPushPermissions,
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
_loading ? CircularProgressIndicator() : SizedBox(width: 0), _loading ? CircularProgressIndicator() : SizedBox(width: 0),
_loading ? SizedBox(width: 10) : SizedBox(width: 0), _loading ? SizedBox(width: 10) : SizedBox(width: 0),
Text('Continue', style: TextStyle(color: Colors.pink)), Text('Continue', style: TextStyle(color: Colors.pink)),
]), ]),
onPressed: _requestPushPermissions,
) )
] ]
) )
@ -121,6 +118,8 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
} }
class OnboardingScreen extends StatefulWidget { class OnboardingScreen extends StatefulWidget {
const OnboardingScreen({super.key});
@override @override
_OnboardingScreenState createState() => _OnboardingScreenState(); State<OnboardingScreen> createState() => _OnboardingScreenState();
} }

View File

@ -4,10 +4,10 @@ import '../util.dart';
class DrawdownPainter extends CustomPainter { class DrawdownPainter extends CustomPainter {
final Map<String,dynamic> pattern; final Map<String,dynamic> pattern;
final double BASE_SIZE; final double baseSize;
@override @override
DrawdownPainter(this.BASE_SIZE, this.pattern) {} DrawdownPainter(this.baseSize, this.pattern);
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
@ -20,10 +20,10 @@ class DrawdownPainter extends CustomPainter {
..strokeWidth = 1; ..strokeWidth = 1;
// Draw grid // Draw grid
for (double i = 0; i <= size.width; i += BASE_SIZE) { for (double i = 0; i <= size.width; i += baseSize) {
canvas.drawLine(Offset(i.toDouble(), size.height), Offset(i.toDouble(), 0), paint); canvas.drawLine(Offset(i.toDouble(), size.height), Offset(i.toDouble(), 0), paint);
} }
for (double y = 0; y <= size.height; y += BASE_SIZE) { for (double y = 0; y <= size.height; y += baseSize) {
canvas.drawLine(Offset(0, y.toDouble()), Offset(size.width, y.toDouble()), paint); canvas.drawLine(Offset(0, y.toDouble()), Offset(size.width, y.toDouble()), paint);
} }
@ -42,9 +42,9 @@ class DrawdownPainter extends CustomPainter {
String threadType = filteredTieup.contains(shaft) ? 'warp' : 'weft'; String threadType = filteredTieup.contains(shaft) ? 'warp' : 'weft';
Rect rect = Offset( Rect rect = Offset(
size.width - BASE_SIZE * (thread + 1), size.width - baseSize * (thread + 1),
tread * BASE_SIZE tread * baseSize
) & Size(BASE_SIZE, BASE_SIZE); ) & Size(baseSize, baseSize);
canvas.drawRect( canvas.drawRect(
rect, rect,
Paint() Paint()

View File

@ -7,40 +7,40 @@ import 'drawdown.dart';
class Pattern extends StatelessWidget { class Pattern extends StatelessWidget {
final Map<String,dynamic> pattern; final Map<String,dynamic> pattern;
final Function? onUpdate; final Function? onUpdate;
final double BASE_SIZE = 5; final double baseSize = 5;
@override @override
Pattern(this.pattern, {this.onUpdate}) {} const Pattern(this.pattern, {super.key, this.onUpdate});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var warp = pattern['warp']; var warp = pattern['warp'];
var weft = pattern['weft']; var weft = pattern['weft'];
double draftWidth = warp['threading']?.length * BASE_SIZE + weft['treadles'] * BASE_SIZE + BASE_SIZE; double draftWidth = warp['threading']?.length * baseSize + weft['treadles'] * baseSize + baseSize;
double draftHeight = warp['shafts'] * BASE_SIZE + weft['treadling']?.length * BASE_SIZE + BASE_SIZE; double draftHeight = warp['shafts'] * baseSize + weft['treadling']?.length * baseSize + baseSize;
double tieupTop = BASE_SIZE; double tieupTop = baseSize;
double tieupRight = BASE_SIZE; double tieupRight = baseSize;
double tieupWidth = weft['treadles'] * BASE_SIZE; double tieupWidth = weft['treadles'] * baseSize;
double tieupHeight = warp['shafts'] * BASE_SIZE; double tieupHeight = warp['shafts'] * baseSize;
double warpTop = 0; double warpTop = 0;
double warpRight = weft['treadles'] * BASE_SIZE + BASE_SIZE * 2; double warpRight = weft['treadles'] * baseSize + baseSize * 2;
double warpWidth = warp['threading']?.length * BASE_SIZE; double warpWidth = warp['threading']?.length * baseSize;
double warpHeight = warp['shafts'] * BASE_SIZE + BASE_SIZE; double warpHeight = warp['shafts'] * baseSize + baseSize;
double weftRight = 0; double weftRight = 0;
double weftTop = warp['shafts'] * BASE_SIZE + BASE_SIZE * 2; double weftTop = warp['shafts'] * baseSize + baseSize * 2;
double weftWidth = weft['treadles'] * BASE_SIZE + BASE_SIZE; double weftWidth = weft['treadles'] * baseSize + baseSize;
double weftHeight = weft['treadling'].length * BASE_SIZE; double weftHeight = weft['treadling'].length * baseSize;
double drawdownTop = warpHeight + BASE_SIZE; double drawdownTop = warpHeight + baseSize;
double drawdownRight = weftWidth + BASE_SIZE; double drawdownRight = weftWidth + baseSize;
double drawdownWidth = warpWidth; double drawdownWidth = warpWidth;
double drawdownHeight = weftHeight; double drawdownHeight = weftHeight;
return Container( return SizedBox(
width: draftWidth, width: draftWidth,
height: draftHeight, height: draftHeight,
child: Stack( child: Stack(
@ -53,8 +53,8 @@ class Pattern extends StatelessWidget {
var tieups = pattern['tieups']; var tieups = pattern['tieups'];
double dx = details.localPosition.dx; double dx = details.localPosition.dx;
double dy = details.localPosition.dy; double dy = details.localPosition.dy;
int tie = (dx / BASE_SIZE).toInt(); int tie = (dx / baseSize).toInt();
int shaft = ((tieupHeight - dy) / BASE_SIZE).toInt() + 1; int shaft = ((tieupHeight - dy) / baseSize).toInt() + 1;
if (tieups[tie].contains(shaft)) { if (tieups[tie].contains(shaft)) {
tieups[tie].remove(shaft); tieups[tie].remove(shaft);
} else { } else {
@ -67,7 +67,7 @@ class Pattern extends StatelessWidget {
}, },
child: CustomPaint( child: CustomPaint(
size: Size(tieupWidth, tieupHeight), size: Size(tieupWidth, tieupHeight),
painter: TieupPainter(BASE_SIZE, this.pattern), painter: TieupPainter(baseSize, pattern),
)), )),
), ),
Positioned( Positioned(
@ -75,7 +75,7 @@ class Pattern extends StatelessWidget {
top: warpTop, top: warpTop,
child: CustomPaint( child: CustomPaint(
size: Size(warpWidth, warpHeight), size: Size(warpWidth, warpHeight),
painter: WarpPainter(BASE_SIZE, this.pattern), painter: WarpPainter(baseSize, pattern),
), ),
), ),
Positioned( Positioned(
@ -83,7 +83,7 @@ class Pattern extends StatelessWidget {
top: weftTop, top: weftTop,
child: CustomPaint( child: CustomPaint(
size: Size(weftWidth, weftHeight), size: Size(weftWidth, weftHeight),
painter: WeftPainter(BASE_SIZE, this.pattern), painter: WeftPainter(baseSize, pattern),
), ),
), ),
Positioned( Positioned(
@ -91,7 +91,7 @@ class Pattern extends StatelessWidget {
top: drawdownTop, top: drawdownTop,
child: CustomPaint( child: CustomPaint(
size: Size(drawdownWidth, drawdownHeight), size: Size(drawdownWidth, drawdownHeight),
painter: DrawdownPainter(BASE_SIZE, this.pattern), painter: DrawdownPainter(baseSize, pattern),
), ),
) )
] ]

View File

@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
class TieupPainter extends CustomPainter { class TieupPainter extends CustomPainter {
final Map<String,dynamic> pattern; final Map<String,dynamic> pattern;
final double BASE_SIZE; final double baseSize;
@override @override
TieupPainter(this.BASE_SIZE, this.pattern) {} TieupPainter(this.baseSize, this.pattern);
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
@ -15,20 +15,20 @@ class TieupPainter extends CustomPainter {
..color = Colors.black..strokeWidth = 0.5; ..color = Colors.black..strokeWidth = 0.5;
// Draw grid // Draw grid
for (double i = 0; i <= size.width; i += BASE_SIZE) { for (double i = 0; i <= size.width; i += baseSize) {
canvas.drawLine(Offset(i.toDouble(), size.height), Offset(i.toDouble(), 0), paint); canvas.drawLine(Offset(i.toDouble(), size.height), Offset(i.toDouble(), 0), paint);
} }
for (double y = 0; y <= size.height; y += BASE_SIZE) { for (double y = 0; y <= size.height; y += baseSize) {
canvas.drawLine(Offset(0, y.toDouble()), Offset(size.width, y.toDouble()), paint); canvas.drawLine(Offset(0, y.toDouble()), Offset(size.width, y.toDouble()), paint);
} }
for (var i = 0; i < tieup.length; i++) { for (var i = 0; i < tieup.length; i++) {
List<dynamic>? tie = tieup[i]; List<dynamic>? tie = tieup[i];
if (tie != null) { if (tie != null) {
for (var j = 0; j < tie!.length; j++) { for (var j = 0; j < tie.length; j++) {
canvas.drawRect( canvas.drawRect(
Offset(i.toDouble()*BASE_SIZE, size.height - (tie[j]*BASE_SIZE)) & Offset(i.toDouble()*baseSize, size.height - (tie[j]*baseSize)) &
Size(BASE_SIZE.toDouble(), BASE_SIZE.toDouble()), Size(baseSize.toDouble(), baseSize.toDouble()),
paint); paint);
} }
} }

View File

@ -4,33 +4,29 @@ import 'pattern.dart';
class PatternViewer extends StatefulWidget { class PatternViewer extends StatefulWidget {
final Map<String,dynamic> pattern; final Map<String,dynamic> pattern;
final bool withEditor; final bool withEditor;
PatternViewer(this.pattern, {this.withEditor = false}) {} const PatternViewer(this.pattern, {super.key, this.withEditor = false});
@override @override
State<PatternViewer> createState() => _PatternViewerState(this.pattern, this.withEditor); State<PatternViewer> createState() => _PatternViewerState();
} }
class _PatternViewerState extends State<PatternViewer> { class _PatternViewerState extends State<PatternViewer> {
Map<String,dynamic> pattern;
final bool withEditor;
bool controllerInitialised = false; bool controllerInitialised = false;
final controller = TransformationController(); final controller = TransformationController();
final double BASE_SIZE = 5; final double baseSize = 5;
_PatternViewerState(this.pattern, this.withEditor) {}
void updatePattern(update) { void updatePattern(update) {
setState(() { setState(() {
pattern!.addAll(update); widget.pattern.addAll(update);
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (!controllerInitialised) { if (!controllerInitialised) {
var warp = pattern['warp']; var warp = widget.pattern['warp'];
var weft = pattern['weft']; var weft = widget.pattern['weft'];
double draftWidth = warp['threading']?.length * BASE_SIZE + weft['treadles'] * BASE_SIZE + BASE_SIZE; double draftWidth = warp['threading']?.length * baseSize + weft['treadles'] * baseSize + baseSize;
final zoomFactor = 1.0; final zoomFactor = 1.0;
final xTranslate = draftWidth - MediaQuery.of(context).size.width - 0; final xTranslate = draftWidth - MediaQuery.of(context).size.width - 0;
final yTranslate = 0.0; final yTranslate = 0.0;
@ -47,7 +43,7 @@ class _PatternViewerState extends State<PatternViewer> {
maxScale: 5, maxScale: 5,
constrained: false, constrained: false,
transformationController: controller, transformationController: controller,
child: RepaintBoundary(child: Pattern(pattern)) child: RepaintBoundary(child: Pattern(widget.pattern))
); );
} }
} }

View File

@ -3,10 +3,10 @@ import '../util.dart';
class WarpPainter extends CustomPainter { class WarpPainter extends CustomPainter {
final Map<String,dynamic> pattern; final Map<String,dynamic> pattern;
final double BASE_SIZE; final double baseSize;
@override @override
WarpPainter(this.BASE_SIZE, this.pattern) {} WarpPainter(this.baseSize, this.pattern);
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
@ -15,17 +15,12 @@ class WarpPainter extends CustomPainter {
var paint = Paint() var paint = Paint()
..color = Colors.black ..color = Colors.black
..strokeWidth = 0.5; ..strokeWidth = 0.5;
var thickPaint = Paint()
..color = Colors.black
..strokeWidth = 1.5;
// Draw grid // Draw grid
int columnsPainted = 0; for (double i = size.width; i >= 0; i -= baseSize) {
for (double i = size.width; i >= 0; i -= BASE_SIZE) {
canvas.drawLine(Offset(i.toDouble(), size.height), Offset(i.toDouble(), 0), paint); canvas.drawLine(Offset(i.toDouble(), size.height), Offset(i.toDouble(), 0), paint);
columnsPainted += 1;
} }
for (double y = 0; y <= size.height; y += BASE_SIZE) { for (double y = 0; y <= size.height; y += baseSize) {
canvas.drawLine(Offset(0, y.toDouble()), Offset(size.width, y.toDouble()), paint); canvas.drawLine(Offset(0, y.toDouble()), Offset(size.width, y.toDouble()), paint);
} }
@ -34,12 +29,12 @@ class WarpPainter extends CustomPainter {
var thread = warp['threading'][i]; var thread = warp['threading'][i];
int? shaft = thread?['shaft']; int? shaft = thread?['shaft'];
String? colour = warp['defaultColour']; String? colour = warp['defaultColour'];
double x = size.width - (i+1)*BASE_SIZE; double x = size.width - (i+1)*baseSize;
if (shaft != null) { if (shaft != null) {
if (shaft! > 0) { if (shaft > 0) {
canvas.drawRect( canvas.drawRect(
Offset(x, size.height - shaft!*BASE_SIZE) & Offset(x, size.height - shaft*baseSize) &
Size(BASE_SIZE.toDouble(), BASE_SIZE.toDouble()), Size(baseSize.toDouble(), baseSize.toDouble()),
paint paint
); );
} }
@ -51,9 +46,9 @@ class WarpPainter extends CustomPainter {
if (colour != null) { if (colour != null) {
canvas.drawRect( canvas.drawRect(
Offset(x, 0) & Offset(x, 0) &
Size(BASE_SIZE.toDouble(), BASE_SIZE.toDouble()), Size(baseSize.toDouble(), baseSize.toDouble()),
Paint() Paint()
..color = Util.rgb(colour!) ..color = Util.rgb(colour)
); );
} }
} }

View File

@ -3,10 +3,10 @@ import '../util.dart';
class WeftPainter extends CustomPainter { class WeftPainter extends CustomPainter {
final Map<String,dynamic> pattern; final Map<String,dynamic> pattern;
final double BASE_SIZE; final double baseSize;
@override @override
WeftPainter(this.BASE_SIZE, this.pattern) {} WeftPainter(this.baseSize, this.pattern);
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
@ -15,29 +15,24 @@ class WeftPainter extends CustomPainter {
var paint = Paint() var paint = Paint()
..color = Colors.black ..color = Colors.black
..strokeWidth = 0.5; ..strokeWidth = 0.5;
var thickPaint = Paint()
..color = Colors.black
..strokeWidth = 1.5;
// Draw grid // Draw grid
int rowsPainted = 0; for (double i = 0; i <= size.width; i += baseSize) {
for (double i = 0; i <= size.width; i += BASE_SIZE) {
canvas.drawLine(Offset(i.toDouble(), size.height), Offset(i.toDouble(), 0), paint); canvas.drawLine(Offset(i.toDouble(), size.height), Offset(i.toDouble(), 0), paint);
} }
for (double y = 0; y <= size.height; y += BASE_SIZE) { for (double y = 0; y <= size.height; y += baseSize) {
canvas.drawLine(Offset(0, y.toDouble()), Offset(size.width, y.toDouble()), paint); canvas.drawLine(Offset(0, y.toDouble()), Offset(size.width, y.toDouble()), paint);
rowsPainted += 1;
} }
for (var i = 0; i < weft['treadling'].length; i++) { for (var i = 0; i < weft['treadling'].length; i++) {
var thread = weft['treadling'][i]; var thread = weft['treadling'][i];
int? treadle = thread?['treadle']; int? treadle = thread?['treadle'];
String? colour = weft['defaultColour']; String? colour = weft['defaultColour'];
double y = i.toDouble()*BASE_SIZE; double y = i.toDouble()*baseSize;
if (treadle != null && treadle! > 0) { if (treadle != null && treadle > 0) {
canvas.drawRect( canvas.drawRect(
Offset((treadle!.toDouble()-1)*BASE_SIZE, y) & Offset((treadle.toDouble()-1)*baseSize, y) &
Size(BASE_SIZE.toDouble(), BASE_SIZE.toDouble()), Size(baseSize.toDouble(), baseSize.toDouble()),
paint paint
); );
} }
@ -46,10 +41,10 @@ class WeftPainter extends CustomPainter {
} }
if (colour != null) { if (colour != null) {
canvas.drawRect( canvas.drawRect(
Offset(size.width - BASE_SIZE, y) & Offset(size.width - baseSize, y) &
Size(BASE_SIZE.toDouble(), BASE_SIZE.toDouble()), Size(baseSize.toDouble(), baseSize.toDouble()),
Paint() Paint()
..color = Util.rgb(colour!) ..color = Util.rgb(colour)
); );
} }
} }

View File

@ -14,9 +14,7 @@ import 'model.dart';
import 'lib.dart'; import 'lib.dart';
class _ProjectScreenState extends State<ProjectScreen> { class _ProjectScreenState extends State<ProjectScreen> {
final String username; late final String fullPath;
final String projectPath;
final String fullPath;
final picker = ImagePicker(); final picker = ImagePicker();
final Api api = Api(); final Api api = Api();
Map<String,dynamic>? project; Map<String,dynamic>? project;
@ -24,19 +22,17 @@ class _ProjectScreenState extends State<ProjectScreen> {
bool _loading = false; bool _loading = false;
Map<String,dynamic>? _creatingObject; Map<String,dynamic>? _creatingObject;
_ProjectScreenState(this.username, this.projectPath, {this.project}) :
fullPath = username + '/' + projectPath;
@override @override
initState() { initState() {
super.initState(); super.initState();
fullPath = '${widget.username}/${widget.projectPath}';
getProject(fullPath); getProject(fullPath);
getObjects(fullPath); getObjects(fullPath);
} }
void getProject(String fullName) async { void getProject(String fullName) async {
setState(() => _loading = true); setState(() => _loading = true);
var data = await api.request('GET', '/projects/' + fullName); var data = await api.request('GET', '/projects/$fullName');
if (data['success'] == true) { if (data['success'] == true) {
setState(() { setState(() {
project = data['payload']; project = data['payload'];
@ -47,7 +43,7 @@ class _ProjectScreenState extends State<ProjectScreen> {
void getObjects(String fullName) async { void getObjects(String fullName) async {
setState(() => _loading = true); setState(() => _loading = true);
var data = await api.request('GET', '/projects/' + fullName + '/objects'); var data = await api.request('GET', '/projects/$fullName/objects');
if (data['success'] == true) { if (data['success'] == true) {
setState(() { setState(() {
_objects = data['payload']['objects']; _objects = data['payload']['objects'];
@ -124,7 +120,7 @@ class _ProjectScreenState extends State<ProjectScreen> {
PlatformFile file = result.files.single; PlatformFile file = result.files.single;
XFile xFile = XFile(file.path!); XFile xFile = XFile(file.path!);
String? ext = file.extension; String? ext = file.extension;
if (ext != null && ext!.toLowerCase() == 'wif' || xFile.name.toLowerCase().contains('.wif')) { if (ext != null && ext.toLowerCase() == 'wif' || xFile.name.toLowerCase().contains('.wif')) {
final String contents = await xFile.readAsString(); final String contents = await xFile.readAsString();
_createObjectFromWif(file.name, contents); _createObjectFromWif(file.name, contents);
} else { } else {
@ -134,16 +130,16 @@ class _ProjectScreenState extends State<ProjectScreen> {
} }
void _chooseImage() async { void _chooseImage() async {
File file;
try { try {
final XFile? imageFile = await picker.pickImage(source: ImageSource.gallery); final XFile? imageFile = await picker.pickImage(source: ImageSource.gallery);
if (imageFile == null) return; if (imageFile == null) return;
final f = new DateFormat('yyyy-MM-dd_hh-mm-ss'); final f = DateFormat('yyyy-MM-dd_hh-mm-ss');
String time = f.format(new DateTime.now()); String time = f.format(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 {
if (!mounted) return;
showDialog( showDialog(
context: context, context: context,
builder: (BuildContext context) => CupertinoAlertDialog( builder: (BuildContext context) => CupertinoAlertDialog(
@ -162,30 +158,30 @@ class _ProjectScreenState extends State<ProjectScreen> {
} }
void showSettingsModal() { void showSettingsModal() {
Widget settingsDialog = new _ProjectSettingsDialog(project!, _onDeleteProject, _onUpdateProject); Widget settingsDialog = _ProjectSettingsDialog(project!, _onDeleteProject, _onUpdateProject);
showCupertinoModalPopup(context: context, builder: (BuildContext context) => settingsDialog); showCupertinoModalPopup(context: context, builder: (BuildContext context) => settingsDialog);
} }
Widget getNetworkImageBox(String url) { Widget getNetworkImageBox(String url) {
return new AspectRatio( return AspectRatio(
aspectRatio: 1 / 1, aspectRatio: 1 / 1,
child: new Container( child: Container(
decoration: new BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0), borderRadius: BorderRadius.circular(10.0),
image: new DecorationImage( image: DecorationImage(
fit: BoxFit.cover, fit: BoxFit.cover,
alignment: FractionalOffset.topCenter, alignment: FractionalOffset.topCenter,
image: new NetworkImage(url), image: NetworkImage(url),
) )
), ),
), ),
); );
} }
Widget getIconBox(Icon icon) { Widget getIconBox(Icon icon) {
return new AspectRatio( return AspectRatio(
aspectRatio: 1 / 1, aspectRatio: 1 / 1,
child: new Container( child: Container(
decoration: new BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[100], color: Colors.grey[100],
borderRadius: BorderRadius.circular(10.0), borderRadius: BorderRadius.circular(10.0),
), ),
@ -236,10 +232,10 @@ class _ProjectScreenState extends State<ProjectScreen> {
leader = CircularProgressIndicator(); leader = CircularProgressIndicator();
} }
return new Card( return Card(
child: InkWell( child: InkWell(
onTap: () { onTap: () {
context.push('/' + username + '/' + projectPath + '/' + object['_id']); context.push('/${widget.username}/${widget.projectPath}/${object["_id"]}');
}, },
child: ListTile( child: ListTile(
leading: leader, leading: leader,
@ -252,17 +248,18 @@ class _ProjectScreenState extends State<ProjectScreen> {
} }
Widget getBody() { Widget getBody() {
if (_loading || project == null) if (_loading || project == null) {
return CircularProgressIndicator(); return CircularProgressIndicator();
else if ((_objects != null && _objects.length > 0) || _creatingObject != null) } else if ((_objects.isNotEmpty) || _creatingObject != null) {
return ListView.builder( return ListView.builder(
itemCount: _objects.length + (_creatingObject != null ? 1 : 0), itemCount: _objects.length + (_creatingObject != null ? 1 : 0),
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
return getObjectCard(index); return getObjectCard(index);
}, },
); );
else } 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.'); 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
@ -329,8 +326,8 @@ class _ProjectScreenState extends State<ProjectScreen> {
SizedBox(width: 10), SizedBox(width: 10),
FloatingActionButton( FloatingActionButton(
heroTag: null, heroTag: null,
child: const Icon(Icons.insert_drive_file_outlined),
onPressed: _chooseFile, onPressed: _chooseFile,
child: const Icon(Icons.insert_drive_file_outlined),
), ),
]), ]),
], ],
@ -342,10 +339,9 @@ class _ProjectScreenState extends State<ProjectScreen> {
class ProjectScreen extends StatefulWidget { class ProjectScreen extends StatefulWidget {
final String username; final String username;
final String projectPath; final String projectPath;
final Map<String,dynamic>? project; const ProjectScreen(this.username, this.projectPath, {super.key});
ProjectScreen(this.username, this.projectPath, {this.project, }) { }
@override @override
_ProjectScreenState createState() => _ProjectScreenState(username, projectPath, project: project); State<ProjectScreen> createState() => _ProjectScreenState();
} }
class _ProjectSettingsDialog extends StatelessWidget { class _ProjectSettingsDialog extends StatelessWidget {
@ -355,7 +351,7 @@ class _ProjectSettingsDialog extends StatelessWidget {
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']; fullPath = '${project["owner"]["username"]}/${project["path"]}';
void _renameProject(BuildContext context) async { void _renameProject(BuildContext context) async {
TextEditingController renameController = TextEditingController(); TextEditingController renameController = TextEditingController();
@ -379,8 +375,9 @@ class _ProjectSettingsDialog extends StatelessWidget {
TextButton( TextButton(
child: Text('OK'), child: Text('OK'),
onPressed: () async { onPressed: () async {
var data = await api.request('PUT', '/projects/' + fullPath, {'name': renameController.text}); var data = await api.request('PUT', '/projects/$fullPath', {'name': renameController.text});
if (data['success']) { if (data['success']) {
if (!context.mounted) return;
context.pop(); context.pop();
_onUpdateProject(data['payload']); _onUpdateProject(data['payload']);
} else { } else {
@ -393,6 +390,7 @@ class _ProjectSettingsDialog extends StatelessWidget {
textColor: Colors.white, textColor: Colors.white,
); );
} }
if (!context.mounted) return;
context.pop(); context.pop();
}, },
), ),
@ -403,15 +401,16 @@ class _ProjectSettingsDialog extends StatelessWidget {
} }
void _toggleVisibility(BuildContext context, bool checked) async { void _toggleVisibility(BuildContext context, bool checked) async {
var data = await api.request('PUT', '/projects/' + fullPath, {'visibility': checked ? 'private': 'public'}); var data = await api.request('PUT', '/projects/$fullPath', {'visibility': checked ? 'private': 'public'});
if (data['success']) { if (data['success']) {
if (!context.mounted) return;
context.pop(); 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/' + fullPath); var data = await api.request('DELETE', '/projects/$fullPath');
if (data['success']) { if (data['success']) {
_onDelete(); _onDelete();
} }
@ -421,8 +420,8 @@ class _ProjectSettingsDialog extends StatelessWidget {
showDialog( showDialog(
context: modalContext, context: modalContext,
builder: (BuildContext context) => CupertinoAlertDialog( builder: (BuildContext context) => CupertinoAlertDialog(
title: new Text('Really delete this project?'), title: Text('Really delete this project?'),
content: new Text('This will remove any files and objects inside the project. This action cannot be undone.'), content: Text('This will remove any files and objects inside the project. This action cannot be undone.'),
actions: <Widget>[ actions: <Widget>[
CupertinoDialogAction( CupertinoDialogAction(
isDefaultAction: true, isDefaultAction: true,
@ -454,7 +453,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),
@ -468,8 +467,8 @@ class _ProjectSettingsDialog extends StatelessWidget {
), ),
CupertinoActionSheetAction( CupertinoActionSheetAction(
onPressed: () { _confirmDeleteProject(context); }, onPressed: () { _confirmDeleteProject(context); },
child: Text('Delete project'),
isDestructiveAction: true, isDestructiveAction: true,
child: Text('Delete project'),
), ),
] ]
); );

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'api.dart'; import 'api.dart';
@ -39,47 +38,42 @@ class _ProjectsTabState extends State<ProjectsTab> {
}); });
} }
void _onCreateProject(newProject) { void _onCreateProject(newProject) {
List<dynamic> _newProjects = _projects; List<dynamic> newProjects = _projects;
_newProjects.insert(0, newProject); newProjects.insert(0, newProject);
setState(() { setState(() {
_projects = _newProjects; _projects = newProjects;
_creatingProject = false; _creatingProject = false;
}); });
} }
void _onDeleteProject(String id) {
List<dynamic> _newProjects = _projects.where((p) => p['_id'] != id).toList();
setState(() {
_projects = _newProjects;
});
}
void showNewProjectDialog() async { void showNewProjectDialog() async {
Widget simpleDialog = new _NewProjectDialog(_onCreatingProject, _onCreateProject); Widget simpleDialog = _NewProjectDialog(_onCreatingProject, _onCreateProject);
showDialog(context: context, builder: (BuildContext context) => simpleDialog); showDialog(context: context, builder: (BuildContext context) => simpleDialog);
} }
Widget buildProjectCard(Map<String,dynamic> project) { Widget buildProjectCard(Map<String,dynamic> project) {
String description = project['description'] != null ? project['description'].replaceAll("\n", " ") : ''; String description = project['description'] != null ? project['description'].replaceAll("\n", " ") : '';
if (description != null && description.length > 80) { if (description.length > 80) {
description = description.substring(0, 77) + '...'; description = '${description.substring(0, 77)}...';
} }
if (project['visibility'] == 'public') { if (project['visibility'] == 'public') {
description = "PUBLIC PROJECT\n" + description; description = "PUBLIC PROJECT\n$description";
} }
else description = "PRIVATE PROJECT\n" + description; else {
return new Card( description = "PRIVATE PROJECT\n$description";
}
return Card(
child: InkWell( child: InkWell(
onTap: () { onTap: () {
context.push('/' + project['owner']['username'] + '/' + project['path']); context.push('/${project["owner"]["username"]}/${project["path"]}');
}, },
child: Container( child: Container(
padding: EdgeInsets.all(5), padding: EdgeInsets.all(5),
child: ListTile( child: ListTile(
leading: new AspectRatio( leading: AspectRatio(
aspectRatio: 1 / 1, aspectRatio: 1 / 1,
child: new Container( child: Container(
decoration: new BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[100], color: Colors.grey[100],
borderRadius: BorderRadius.circular(10.0), borderRadius: BorderRadius.circular(10.0),
), ),
@ -87,7 +81,7 @@ class _ProjectsTabState extends State<ProjectsTab> {
), ),
), ),
trailing: Icon(Icons.keyboard_arrow_right), trailing: Icon(Icons.keyboard_arrow_right),
title: Text(project['name'] != null ? project['name'] : 'Untitled project'), title: Text(project['name'] ?? 'Untitled project'),
subtitle: Text(description), subtitle: Text(description),
), ),
)) ))
@ -97,26 +91,29 @@ class _ProjectsTabState extends State<ProjectsTab> {
Widget getBody() { Widget getBody() {
AppModel model = Provider.of<AppModel>(context); AppModel model = Provider.of<AppModel>(context);
if (model.user == null) if (model.user == null) {
return LoginNeeded(text: 'Once logged in, you\'ll find your own projects shown here.'); return LoginNeeded(text: 'Once logged in, you\'ll find your own projects shown here.');
if (_loading) }
if (_loading) {
return CircularProgressIndicator(); return CircularProgressIndicator();
else if (_projects != null && _projects.length > 0) } else if (_projects.isNotEmpty) {
return ListView.builder( return ListView.builder(
itemCount: _projects.length, itemCount: _projects.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
return buildProjectCard(_projects[index]); return buildProjectCard(_projects[index]);
}, },
); );
else return Column( } else {
crossAxisAlignment: CrossAxisAlignment.center, return Column(
mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ mainAxisAlignment: MainAxisAlignment.center,
Text('Create your first project', style: TextStyle(fontSize: 20), textAlign: TextAlign.center), children: [
Image(image: AssetImage('assets/reading.png'), width: 300), Text('Create your first project', style: TextStyle(fontSize: 20), textAlign: TextAlign.center),
Text('Projects contain all the files and patterns that make up a piece of work. Create one using the + button below.', 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
@ -156,20 +153,17 @@ class _ProjectsTabState extends State<ProjectsTab> {
} }
class ProjectsTab extends StatefulWidget { class ProjectsTab extends StatefulWidget {
const ProjectsTab({super.key});
@override @override
_ProjectsTabState createState() => _ProjectsTabState(); State<ProjectsTab> createState() => _ProjectsTabState();
} }
class _NewProjectDialogState extends State<_NewProjectDialog> { class _NewProjectDialogState extends State<_NewProjectDialog> {
final TextEditingController _newProjectNameController = TextEditingController(); final TextEditingController _newProjectNameController = TextEditingController();
final Function _onStart;
final Function _onComplete;
String _newProjectName = '';
bool _newProjectPrivate = false; bool _newProjectPrivate = false;
final Api api = Api(); final Api api = Api();
_NewProjectDialogState(this._onStart, this._onComplete) {}
void _onToggleProjectVisibility(checked) { void _onToggleProjectVisibility(checked) {
setState(() { setState(() {
_newProjectPrivate = checked; _newProjectPrivate = checked;
@ -177,13 +171,15 @@ class _NewProjectDialogState extends State<_NewProjectDialog> {
} }
void _createProject() async { void _createProject() async {
_onStart(); widget._onStart();
String name = _newProjectNameController.text; String name = _newProjectNameController.text;
bool priv = _newProjectPrivate; bool priv = _newProjectPrivate;
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']); widget._onComplete(data['payload']);
context.pop(); if (mounted) {
context.pop();
}
} }
} }
@ -231,7 +227,7 @@ class _NewProjectDialogState extends State<_NewProjectDialog> {
class _NewProjectDialog extends StatefulWidget { class _NewProjectDialog extends StatefulWidget {
final Function _onComplete; final Function _onComplete;
final Function _onStart; final Function _onStart;
_NewProjectDialog(this._onStart, this._onComplete) {} const _NewProjectDialog(this._onStart, this._onComplete);
@override @override
_NewProjectDialogState createState() => _NewProjectDialogState(_onStart, _onComplete); State<_NewProjectDialog> createState() => _NewProjectDialogState();
} }

View File

@ -19,16 +19,19 @@ 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) {
if (!context.mounted) return;
AppModel model = Provider.of<AppModel>(context, listen: false); AppModel model = Provider.of<AppModel>(context, listen: false);
await model.setToken(data['payload']['token']); await model.setToken(data['payload']['token']);
if (!context.mounted) return;
context.go('/onboarding'); context.go('/onboarding');
} }
else { else {
if (!context.mounted) return;
showDialog( showDialog(
context: context, context: context,
builder: (BuildContext context) => CupertinoAlertDialog( builder: (BuildContext context) => CupertinoAlertDialog(
title: new Text("There was a problem registering your account"), title: Text("There was a problem registering your account"),
content: new Text(data['message']), content: Text(data['message']),
actions: <Widget>[ actions: <Widget>[
CupertinoDialogAction( CupertinoDialogAction(
isDefaultAction: true, isDefaultAction: true,
@ -84,9 +87,9 @@ class _RegisterScreenState extends State<RegisterScreen> {
text: 'By registering you agree to Treadl\'s ', text: 'By registering you agree to Treadl\'s ',
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
children: <TextSpan>[ 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: 'Terms of Use', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.pink), recognizer: TapGestureRecognizer()..onTap = () => launchUrl(Uri.parse('https://treadl.com/terms-of-use'))),
TextSpan(text: ' and '), 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: 'Privacy Policy', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.pink), recognizer: TapGestureRecognizer()..onTap = () => launchUrl(Uri.parse('https://treadl.com/privacy'))),
TextSpan(text: '.'), TextSpan(text: '.'),
], ],
), ),
@ -106,6 +109,8 @@ class _RegisterScreenState extends State<RegisterScreen> {
} }
class RegisterScreen extends StatefulWidget { class RegisterScreen extends StatefulWidget {
const RegisterScreen({super.key});
@override @override
_RegisterScreenState createState() => _RegisterScreenState(); State<RegisterScreen> createState() => _RegisterScreenState();
} }

View File

@ -8,6 +8,8 @@ import 'model.dart';
class SettingsScreen extends StatelessWidget { class SettingsScreen extends StatelessWidget {
final TextEditingController _passwordController = TextEditingController(); final TextEditingController _passwordController = TextEditingController();
SettingsScreen({super.key});
void _logout(BuildContext context) async { void _logout(BuildContext context) async {
AppModel model = Provider.of<AppModel>(context, listen: false); AppModel model = Provider.of<AppModel>(context, listen: false);
@ -44,16 +46,18 @@ 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) {
if (!context.mounted) return;
AppModel model = Provider.of<AppModel>(context, listen: false); AppModel model = Provider.of<AppModel>(context, listen: false);
model.setToken(null); model.setToken(null);
model.setUser(null); model.setUser(null);
context.go('/home'); context.go('/home');
} else { } else {
if (!context.mounted) return;
showDialog( showDialog(
context: context, context: context,
builder: (BuildContext context) => CupertinoAlertDialog( builder: (BuildContext context) => CupertinoAlertDialog(
title: new Text('There was a problem with deleting your account'), title: Text('There was a problem with deleting your account'),
content: new Text(data['message']), content: Text(data['message']),
actions: <Widget>[ actions: <Widget>[
CupertinoDialogAction( CupertinoDialogAction(
isDefaultAction: true, isDefaultAction: true,
@ -93,9 +97,7 @@ class SettingsScreen extends StatelessWidget {
child: child:
Text('Thanks for using Treadl', style: Theme.of(context).textTheme.titleLarge), Text('Thanks for using Treadl', style: Theme.of(context).textTheme.titleLarge),
), ),
Container( Text("Treadl is an app for managing your projects and for keeping in touch with your weaving communities.\n\nWe're always trying to make Treadl better, so if you have any feedback please let us know!", style: Theme.of(context).textTheme.bodyMedium),
child: Text("Treadl is an app for managing your projects and for keeping in touch with your weaving communities.\n\nWe're always trying to make Treadl better, so if you have any feedback please let us know!", style: Theme.of(context).textTheme.bodyMedium)
),
SizedBox(height: 30), SizedBox(height: 30),
@ -136,26 +138,26 @@ class SettingsScreen extends StatelessWidget {
'body': '' 'body': ''
}, },
); );
launch(emailLaunchUri.toString()); launchUrl(Uri.parse(emailLaunchUri.toString()));
} }
), ),
ListTile( ListTile(
leading: Icon(Icons.link), leading: Icon(Icons.link),
trailing: Icon(Icons.explore), trailing: Icon(Icons.explore),
title: Text('Visit Our Website'), title: Text('Visit Our Website'),
onTap: () => launch('https://treadl.com'), onTap: () => launchUrl(Uri.parse('https://treadl.com')),
), ),
ListTile( ListTile(
leading: Icon(Icons.insert_drive_file), leading: Icon(Icons.insert_drive_file),
trailing: Icon(Icons.explore), trailing: Icon(Icons.explore),
title: Text('Terms of Use'), title: Text('Terms of Use'),
onTap: () => launch('https://treadl.com/terms-of-use'), onTap: () => launchUrl(Uri.parse('https://treadl.com/terms-of-use')),
), ),
ListTile( ListTile(
leading: Icon(Icons.insert_drive_file), leading: Icon(Icons.insert_drive_file),
trailing: Icon(Icons.explore), trailing: Icon(Icons.explore),
title: Text('Privacy Policy'), title: Text('Privacy Policy'),
onTap: () => launch('https://treadl.com/privacy'), onTap: () => launchUrl(Uri.parse('https://treadl.com/privacy')),
), ),
] ]
), ),

View File

@ -1,6 +1,5 @@
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:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'util.dart'; import 'util.dart';
@ -8,24 +7,21 @@ import 'api.dart';
import 'lib.dart'; import 'lib.dart';
class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateMixin { class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateMixin {
final String username;
final Api api = Api(); final Api api = Api();
TabController? _tabController; TabController? _tabController;
Map<String,dynamic>? _user; Map<String,dynamic>? _user;
bool _loading = false; bool _loading = false;
_UserScreenState(this.username) { }
@override @override
initState() { initState() {
super.initState(); super.initState();
_tabController = new TabController(length: 2, vsync: this); _tabController = TabController(length: 2, vsync: this);
getUser(username); getUser(widget.username);
} }
void getUser(String username) async { void getUser(String username) async {
if (username == null) return;
setState(() => _loading = true); setState(() => _loading = true);
var data = await api.request('GET', '/users/' + username); var data = await api.request('GET', '/users/$username');
if (data['success'] == true) { if (data['success'] == true) {
setState(() { setState(() {
_user = data['payload']; _user = data['payload'];
@ -35,9 +31,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
} }
Widget getBody() { Widget getBody() {
if (_loading) if (_loading) {
return CircularProgressIndicator(); return CircularProgressIndicator();
else if (_user != null && _tabController != null) { } else if (_user != null && _tabController != null) {
var u = _user!; var u = _user!;
String? created; String? created;
if (u['createdAt'] != null) { if (u['createdAt'] != null) {
@ -63,7 +59,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
Text(u['location']) Text(u['location'])
]) : SizedBox(height: 1), ]) : SizedBox(height: 1),
SizedBox(height: 10), SizedBox(height: 10),
Text('Member' + (created != null ? (' since ' + created!) : ''), Text('Member${created != null ? (' since $created') : ''}',
style: TextStyle(color: Colors.grey[500]) style: TextStyle(color: Colors.grey[500])
), ),
SizedBox(height: 10), SizedBox(height: 10),
@ -72,9 +68,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
onTap: () { onTap: () {
String url = u['website']; String url = u['website'];
if (!url.startsWith('http')) { if (!url.startsWith('http')) {
url = 'http://' + url; url = 'http://$url';
} }
launch(url); launchUrl(Uri.parse(url));
}, },
child: Text(u['website'], child: Text(u['website'],
style: TextStyle(color: Colors.pink)) style: TextStyle(color: Colors.pink))
@ -134,8 +130,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
) )
]); ]);
} }
else else {
return Text('User not found'); return Text('User not found');
}
} }
@override @override
@ -143,12 +140,12 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(username), title: Text(widget.username),
actions: <Widget>[ actions: <Widget>[
IconButton( IconButton(
icon: Icon(Icons.person), icon: Icon(Icons.person),
onPressed: () { onPressed: () {
launch('https://www.treadl.com/' + username); launchUrl(Uri.parse('https://www.treadl.com/${widget.username}'));
}, },
), ),
] ]
@ -164,7 +161,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
class UserScreen extends StatefulWidget { class UserScreen extends StatefulWidget {
final String username; final String username;
UserScreen(this.username) { } const UserScreen(this.username, {super.key});
@override @override
_UserScreenState createState() => _UserScreenState(username); State<UserScreen> createState() => _UserScreenState();
} }

View File

@ -2,15 +2,14 @@ import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart'; 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 'model.dart'; import 'model.dart';
String APP_URL = 'https://www.treadl.com'; String appBaseUrl = 'https://www.treadl.com';
class Util { class Util {
static ImageProvider? avatarUrl(Map<String,dynamic> user) { static ImageProvider? avatarUrl(Map<String,dynamic> user) {
if (user != null && user['avatar'] != null) { if (user['avatar'] != null) {
if (user['avatar'].length < 3) { if (user['avatar'].length < 3) {
return AssetImage('assets/avatars/${user['avatar']}.png'); return AssetImage('assets/avatars/${user['avatar']}.png');
} }
@ -23,19 +22,19 @@ class Util {
static Widget avatarImage(ImageProvider? image, {double size=30}) { static Widget avatarImage(ImageProvider? image, {double size=30}) {
if (image != null) { if (image != null) {
return new Container( return Container(
width: size, width: size,
height: size, height: size,
decoration: new BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
image: new DecorationImage( image: DecorationImage(
fit: BoxFit.fill, fit: BoxFit.fill,
image: image image: image
) )
) )
); );
} }
return new Container( return Container(
width: size, width: size,
height: size, height: size,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -54,7 +53,7 @@ class Util {
} }
static String appUrl(String path) { static String appUrl(String path) {
return APP_URL + '/' + path; return '$appBaseUrl/$path';
} }
static Future<String> storagePath() async { static Future<String> storagePath() async {

View File

@ -1,6 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
@ -9,17 +7,18 @@ import 'model.dart';
import 'lib.dart'; import 'lib.dart';
class _VerifyEmailScreenState extends State<VerifyEmailScreen> { class _VerifyEmailScreenState extends State<VerifyEmailScreen> {
final String? token; String? token;
bool loading = false; bool loading = false;
String? error; String? error;
String? success; String? success;
Api api = Api(); Api api = Api();
_VerifyEmailScreenState({required this.token}); _VerifyEmailScreenState();
@override @override
initState() { initState() {
super.initState(); super.initState();
_verify(context); _verify(context);
token = widget.token;
} }
void _verify(BuildContext context) async { void _verify(BuildContext context) async {
@ -48,6 +47,7 @@ class _VerifyEmailScreenState extends State<VerifyEmailScreen> {
textColor: Colors.white, textColor: Colors.white,
fontSize: 20.0 fontSize: 20.0
); );
if (!context.mounted) return;
context.go('/'); context.go('/');
} else { } else {
setState(() { setState(() {
@ -147,6 +147,7 @@ class _VerifyEmailScreenState extends State<VerifyEmailScreen> {
class VerifyEmailScreen extends StatefulWidget { class VerifyEmailScreen extends StatefulWidget {
final String? token; final String? token;
@override @override
VerifyEmailScreen({required this.token}); const VerifyEmailScreen({super.key, required this.token});
_VerifyEmailScreenState createState() => _VerifyEmailScreenState(token: token); @override
State<VerifyEmailScreen> createState() => _VerifyEmailScreenState();
} }

View File

@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'login.dart';
class WelcomeScreen extends StatelessWidget { class WelcomeScreen extends StatelessWidget {
const WelcomeScreen({super.key});
void _login(BuildContext context) { void _login(BuildContext context) {
context.push('/login'); context.push('/login');
} }
@ -27,7 +27,7 @@ class WelcomeScreen extends StatelessWidget {
SizedBox(height: 30), SizedBox(height: 30),
ElevatedButton( ElevatedButton(
onPressed: () => _login(context), onPressed: () => _login(context),
child: new Text("Login", child: Text("Login",
style: TextStyle(color: Colors.pink), style: TextStyle(color: Colors.pink),
textAlign: TextAlign.center, textAlign: TextAlign.center,
) )
@ -35,14 +35,14 @@ class WelcomeScreen extends StatelessWidget {
SizedBox(height: 15), SizedBox(height: 15),
ElevatedButton( ElevatedButton(
onPressed: () => _register(context), onPressed: () => _register(context),
child: new Text("Register", child: Text("Register",
textAlign: TextAlign.center, textAlign: TextAlign.center,
) )
), ),
SizedBox(height: 35), SizedBox(height: 35),
TextButton( TextButton(
onPressed: () => context.pop(), onPressed: () => context.pop(),
child: new Text("Cancel", child: Text("Cancel",
style: TextStyle(color: Colors.white), style: TextStyle(color: Colors.white),
textAlign: TextAlign.center, textAlign: TextAlign.center,
) )

View File

@ -5,10 +5,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _flutterfire_internals name: _flutterfire_internals
sha256: "7fd72d77a7487c26faab1d274af23fb008763ddc10800261abbfb2c067f183d5" sha256: de9ecbb3ddafd446095f7e833c853aff2fa1682b017921fe63a833f9d6f0e422
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.53" version: "1.3.54"
archive: archive:
dependency: transitive dependency: transitive
description: description:
@ -93,10 +93,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: crypto name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.3" version: "3.0.6"
csslib: csslib:
dependency: transitive dependency: transitive
description: description:
@ -133,58 +133,58 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: file name: file
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.4" version: "7.0.1"
file_picker: file_picker:
dependency: "direct main" dependency: "direct main"
description: description:
name: file_picker name: file_picker
sha256: "8d938fd5c11dc81bf1acd4f7f0486c683fe9e79a0b13419e27730f9ce4d8a25b" sha256: "09b474c0c8117484b80cbebc043801ff91e05cfbd2874d512825c899e1754694"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.2.1" version: "9.2.3"
file_selector_linux: file_selector_linux:
dependency: transitive dependency: transitive
description: description:
name: file_selector_linux name: file_selector_linux
sha256: d17c5e450192cdc40b718804dfb4eaf79a71bed60ee9530703900879ba50baa3 sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.9.1+3" version: "0.9.3+2"
file_selector_macos: file_selector_macos:
dependency: transitive dependency: transitive
description: description:
name: file_selector_macos name: file_selector_macos
sha256: "6290eec24fc4cc62535fe609e0c6714d3c1306191dc8c3b0319eaecc09423a3a" sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.9.2" version: "0.9.4+2"
file_selector_platform_interface: file_selector_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: file_selector_platform_interface name: file_selector_platform_interface
sha256: "2a7f4bbf7bd2f022ecea85bfb1754e87f7dd403a9abc17a84a4fa2ddfe2abc0a" sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.1" version: "2.6.2"
file_selector_windows: file_selector_windows:
dependency: transitive dependency: transitive
description: description:
name: file_selector_windows name: file_selector_windows
sha256: ef246380b66d1fb9089fc65622c387bf3780bca79f533424c31d07f12c2c7fd8 sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.9.2" version: "0.9.3+4"
firebase_core: firebase_core:
dependency: transitive dependency: "direct main"
description: description:
name: firebase_core name: firebase_core
sha256: f4d8f49574a4e396f34567f3eec4d38ab9c3910818dec22ca42b2a467c685d8b sha256: "017d17d9915670e6117497e640b2859e0b868026ea36bf3a57feb28c3b97debe"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.12.1" version: "3.13.0"
firebase_core_platform_interface: firebase_core_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -197,34 +197,42 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: firebase_core_web name: firebase_core_web
sha256: faa5a76f6380a9b90b53bc3bdcb85bc7926a382e0709b9b5edac9f7746651493 sha256: "129a34d1e0fb62e2b488d988a1fc26cc15636357e50944ffee2862efe8929b23"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.21.1" version: "2.22.0"
firebase_messaging: firebase_messaging:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_messaging name: firebase_messaging
sha256: "5fc345c6341f9dc69fd0ffcbf508c784fd6d1b9e9f249587f30434dd8b6aa281" sha256: "5f8918848ee0c8eb172fc7698619b2bcd7dda9ade8b93522c6297dd8f9178356"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.2.4" version: "15.2.5"
firebase_messaging_platform_interface: firebase_messaging_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: firebase_messaging_platform_interface name: firebase_messaging_platform_interface
sha256: a935924cf40925985c8049df4968b1dde5c704f570f3ce380b31d3de6990dd94 sha256: "0bbea00680249595fc896e7313a2bd90bd55be6e0abbe8b9a39d81b6b306acb6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.6.4" version: "4.6.5"
firebase_messaging_web: firebase_messaging_web:
dependency: transitive dependency: transitive
description: description:
name: firebase_messaging_web name: firebase_messaging_web
sha256: fafebf6a1921931334f3f10edb5037a5712288efdd022881e2d093e5654a2fd4 sha256: ffb392ce2a7e8439cd0a9a80e3c702194e73c927e5c7b4f0adf6faa00b245b17
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.10.4" version: "3.10.5"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -234,10 +242,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_expandable_fab name: flutter_expandable_fab
sha256: b14caf78720a48f650e6e1a38d724e33b1f5348d646fa1c266570c31a7f87ef3 sha256: "4d03f54e5384897e32606e9959cef5e7857e5a203e24684f95dfbb5f7fb9b88e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.0" version: "2.4.1"
flutter_html: flutter_html:
dependency: "direct main" dependency: "direct main"
description: description:
@ -254,6 +262,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.14.3" version: "0.14.3"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@ -308,10 +324,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: http_parser name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.2" version: "4.1.2"
image: image:
dependency: transitive dependency: transitive
description: description:
@ -332,42 +348,42 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image_picker_android name: image_picker_android
sha256: "1a27bf4cc0330389cebe465bab08fe6dec97e44015b4899637344bb7297759ec" sha256: "8bd392ba8b0c8957a157ae0dc9fcf48c58e6c20908d5880aea1d79734df090e9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.8.9+2" version: "0.8.12+22"
image_picker_for_web: image_picker_for_web:
dependency: transitive dependency: transitive
description: description:
name: image_picker_for_web name: image_picker_for_web
sha256: "869fe8a64771b7afbc99fc433a5f7be2fea4d1cb3d7c11a48b6b579eb9c797f0" sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "3.0.6"
image_picker_ios: image_picker_ios:
dependency: transitive dependency: transitive
description: description:
name: image_picker_ios name: image_picker_ios
sha256: eac0a62104fa12feed213596df0321f57ce5a572562f72a68c4ff81e9e4caacf sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.8.9" version: "0.8.12+2"
image_picker_linux: image_picker_linux:
dependency: transitive dependency: transitive
description: description:
name: image_picker_linux name: image_picker_linux
sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.1+1" version: "0.2.1+2"
image_picker_macos: image_picker_macos:
dependency: transitive dependency: transitive
description: description:
name: image_picker_macos name: image_picker_macos
sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.1+1" version: "0.2.1+2"
image_picker_platform_interface: image_picker_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -424,6 +440,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.1"
lints:
dependency: transitive
description:
name: lints
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
url: "https://pub.dev"
source: hosted
version: "5.1.1"
list_counter: list_counter:
dependency: transitive dependency: transitive
description: description:
@ -436,10 +460,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: logging name: logging
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.3.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@ -468,10 +492,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: mime name: mime
sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.4" version: "2.0.0"
nested: nested:
dependency: transitive dependency: transitive
description: description:
@ -524,34 +548,34 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_platform_interface name: path_provider_platform_interface
sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.1.2"
path_provider_windows: path_provider_windows:
dependency: transitive dependency: transitive
description: description:
name: path_provider_windows name: path_provider_windows
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.1" version: "2.3.0"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
name: petitparser name: petitparser
sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.1.0" version: "6.1.0"
platform: platform:
dependency: transitive dependency: transitive
description: description:
name: platform name: platform
sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.0" version: "3.1.6"
plugin_platform_interface: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -568,22 +592,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.1" version: "6.0.1"
process:
dependency: transitive
description:
name: process
sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09"
url: "https://pub.dev"
source: hosted
version: "4.2.4"
provider: provider:
dependency: "direct main" dependency: "direct main"
description: description:
name: provider name: provider
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c sha256: "489024f942069c2920c844ee18bb3d467c69e48955a4f32d1677f71be103e310"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.2" version: "6.1.4"
share_plus: share_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@ -604,10 +620,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: shared_preferences name: shared_preferences
sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.2" version: "2.5.3"
shared_preferences_android: shared_preferences_android:
dependency: transitive dependency: transitive
description: description:
@ -721,10 +737,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: typed_data name: typed_data
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.2" version: "1.4.0"
url_launcher: url_launcher:
dependency: "direct main" dependency: "direct main"
description: description:
@ -745,10 +761,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_ios name: url_launcher_ios
sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.3.2" version: "6.3.3"
url_launcher_linux: url_launcher_linux:
dependency: transitive dependency: transitive
description: description:
@ -793,10 +809,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: uuid name: uuid
sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f" sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2.2" version: "4.5.1"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@ -833,26 +849,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: xdg_directories name: xdg_directories
sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.1.0"
xml: xml:
dependency: transitive dependency: transitive
description: description:
name: xml name: xml
sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.2.2" version: "6.5.0"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:
name: yaml name: yaml
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.2" version: "3.1.3"
sdks: sdks:
dart: ">=3.7.0 <4.0.0" dart: ">=3.7.0 <4.0.0"
flutter: ">=3.27.0" flutter: ">=3.27.0"

View File

@ -40,9 +40,11 @@ dependencies:
go_router: ^14.8.1 go_router: ^14.8.1
fluttertoast: ^8.2.12 fluttertoast: ^8.2.12
firebase_core: any
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^5.0.0
flutter_icons: flutter_icons:
android: "launcher_icon" android: "launcher_icon"