Compare commits

..

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

29 changed files with 1010 additions and 2063 deletions

View File

@ -32,8 +32,6 @@ def get(user, id):
if obj['type'] == 'pattern' and 'preview' in obj and '.png' in obj['preview']:
obj['previewUrl'] = uploads.get_presigned_url('projects/{0}/{1}'.format(proj['_id'], obj['preview']))
del obj['preview']
if obj.get('fullPreview'):
obj['fullPreviewUrl'] = uploads.get_presigned_url('projects/{0}/{1}'.format(proj['_id'], obj['fullPreview']))
return obj
def copy_to_project(user, id, project_id):
@ -56,9 +54,6 @@ def copy_to_project(user, id, project_id):
obj['createdAt'] = datetime.datetime.now()
obj['commentCount'] = 0
if 'preview' in obj: del obj['preview']
if obj.get('pattern'):
images = wif.generate_images(obj)
if images: obj.update(images)
db.objects.insert_one(obj)
return obj
@ -105,15 +100,7 @@ def update(user, id, data):
if not project: raise util.errors.NotFound('Project not found')
if not util.can_edit_project(user, project):
raise util.errors.Forbidden('Forbidden')
allowed_keys = ['name', 'description', 'pattern']
if data.get('pattern'):
obj.update(data)
images = wif.generate_images(obj)
if images:
data.update(images)
allowed_keys += ['preview', 'fullPreview']
allowed_keys = ['name', 'description', 'pattern', 'preview']
updater = util.build_updater(data, allowed_keys)
if updater:
db.objects.update({'_id': ObjectId(id)}, updater)

View File

@ -1,7 +1,7 @@
import datetime, re
from bson.objectid import ObjectId
from util import database, wif, util
from api import uploads, objects
from api import uploads
default_pattern = {
'warp': {
@ -118,15 +118,13 @@ def get_objects(user, username, path):
if not util.can_view_project(user, project):
raise util.errors.Forbidden('This project is private')
objs = list(db.objects.find({'project': project['_id']}, {'createdAt': 1, 'name': 1, 'description': 1, 'project': 1, 'preview': 1, 'fullPreview': 1, 'type': 1, 'storedName': 1, 'isImage': 1, 'imageBlurHash': 1, 'commentCount': 1}))
objs = list(db.objects.find({'project': project['_id']}, {'createdAt': 1, 'name': 1, 'description': 1, 'project': 1, 'preview': 1, 'type': 1, 'storedName': 1, 'isImage': 1, 'imageBlurHash': 1, 'commentCount': 1}))
for obj in objs:
if obj['type'] == 'file' and 'storedName' in obj:
obj['url'] = uploads.get_presigned_url('projects/{0}/{1}'.format(project['_id'], obj['storedName']))
if obj['type'] == 'pattern' and 'preview' in obj and '.png' in obj['preview']:
obj['previewUrl'] = uploads.get_presigned_url('projects/{0}/{1}'.format(project['_id'], obj['preview']))
del obj['preview']
if obj.get('fullPreview'):
obj['fullPreviewUrl'] = uploads.get_presigned_url('projects/{0}/{1}'.format(project['_id'], obj['fullPreview']))
return objs
def create_object(user, username, path, data):
@ -158,32 +156,36 @@ def create_object(user, username, path, data):
uploads.blur_image('projects/' + str(project['_id']) + '/' + data['storedName'], handle_cb)
return obj
if data['type'] == 'pattern':
obj = {
'project': project['_id'],
'createdAt': datetime.datetime.now(),
'type': 'pattern',
}
if data.get('wif'):
try:
pattern = wif.loads(data['wif'])
if pattern:
obj['name'] = pattern['name']
obj['pattern'] = pattern
obj = {
'project': project['_id'],
'name': pattern['name'],
'createdAt': datetime.datetime.now(),
'type': 'pattern',
'pattern': pattern
}
result = db.objects.insert_one(obj)
obj['_id'] = result.inserted_id
return obj
except Exception as e:
raise util.errors.BadRequest('Unable to load WIF file. It is either invalid or in a format we cannot understand.')
else:
elif data.get('name'):
pattern = default_pattern.copy()
pattern['warp'].update({'shafts': data.get('shafts', 8)})
pattern['weft'].update({'treadles': data.get('treadles', 8)})
obj['name'] = data.get('name') or 'Untitled Pattern'
obj['pattern'] = pattern
obj = {
'project': project['_id'],
'name': data['name'],
'createdAt': datetime.datetime.now(),
'type': 'pattern',
'pattern': pattern
}
result = db.objects.insert_one(obj)
obj['_id'] = result.inserted_id
images = wif.generate_images(obj)
if images:
db.objects.update_one({'_id': obj['_id']}, {'$set': images})
return objects.get(user, obj['_id'])
return obj
raise util.errors.BadRequest('Unable to create object')

View File

@ -32,14 +32,6 @@ def get_presigned_url(path):
}
)
def upload_file(path, data):
s3 = get_s3()
s3.upload_fileobj(
data,
os.environ['AWS_S3_BUCKET'],
path,
)
def get_file(key):
s3 = get_s3()
return s3.get_object(

1663
api/poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,6 @@ blurhash-python = "^1.0.2"
gunicorn = "^20.0.4"
sentry-sdk = {extras = ["flask"], version = "^1.5.10"}
pyOpenSSL = "^22.0.0"
pillow = "^10.2.0"
[tool.poetry.dev-dependencies]

View File

@ -1,7 +1,4 @@
import io, time
import configparser
from PIL import Image, ImageDraw
from api import uploads
def normalise_colour(max_color, triplet):
color_factor = 256/max_color
@ -19,19 +16,6 @@ def denormalise_colour(max_color, triplet):
new_components.append(str(int(float(color_factor) * int(component))))
return ','.join(new_components)
def colour_tuple(triplet):
if not triplet: return None
components = triplet.split(',')
return tuple(map(lambda c: int(c), components))
def darken_colour(c_tuple, val):
def darken(c):
c = c * val
if c < 0: c = 0
if c > 255: c = 255
return int(c)
return tuple(map(darken, c_tuple))
def get_colour_index(colours, colour):
for (index, c) in enumerate(colours):
if c == colour: return index + 1
@ -213,189 +197,3 @@ def loads(wif_file):
draft['tieups'][int(x)-1] = []
return draft
def generate_images(obj):
try:
return {
'preview': draw_image(obj),
'fullPreview': draw_image(obj, with_plan=True)
}
except Exception as e:
print(e)
return {}
def draw_image(obj, with_plan=False):
if not obj or not obj['pattern']: raise Exception('Invalid pattern')
BASE_SIZE = 10
pattern = obj['pattern']
warp = pattern['warp']
weft = pattern['weft']
tieups = pattern['tieups']
full_width = len(warp['threading']) * BASE_SIZE + BASE_SIZE + weft['treadles'] * BASE_SIZE + BASE_SIZE if with_plan else len(warp['threading']) * BASE_SIZE
full_height = warp['shafts'] * BASE_SIZE + len(weft['treadling']) * BASE_SIZE + BASE_SIZE * 2 if with_plan else len(weft['treadling']) * BASE_SIZE
warp_top = 0
warp_left = 0
warp_right = len(warp['threading']) * BASE_SIZE
warp_bottom = warp['shafts'] * BASE_SIZE + BASE_SIZE
weft_left = warp_right + BASE_SIZE
weft_top = warp['shafts'] * BASE_SIZE + BASE_SIZE * 2
weft_right = warp_right + BASE_SIZE + weft['treadles'] * BASE_SIZE + BASE_SIZE
weft_bottom = weft_top + len(weft['treadling']) * BASE_SIZE
tieup_left = warp_right + BASE_SIZE
tieup_top = BASE_SIZE
tieup_right = tieup_left + weft['treadles'] * BASE_SIZE
tieup_bottom = warp_bottom
drawdown_top = warp_bottom + BASE_SIZE if with_plan else 0
drawdown_right = warp_right if with_plan else full_width
drawdown_left = warp_left if with_plan else 0
drawdown_bottom = weft_bottom if with_plan else full_height
WHITE=(255,255,255)
GREY = (150,150,150)
BLACK = (0,0,0)
img = Image.new("RGBA", (full_width, full_height), WHITE)
draw = ImageDraw.Draw(img)
# Draw warp
if with_plan:
draw.rectangle([
(warp_left, warp_top),
(warp_right, warp_bottom)
], fill=None, outline=GREY, width=1)
for y in range(1, warp['shafts'] + 1):
ycoord = y * BASE_SIZE
draw.line([
(warp_left, ycoord),
(warp_right, ycoord),
],
fill=GREY, width=1, joint=None)
for (i, x) in enumerate(range(len(warp['threading'])-1, 0, -1)):
thread = warp['threading'][i]
xcoord = x * BASE_SIZE
draw.line([
(xcoord, warp_top),
(xcoord, warp_bottom),
],
fill=GREY, width=1, joint=None)
if thread.get('shaft', 0) > 0:
ycoord = warp_bottom - (thread['shaft'] * BASE_SIZE)
draw.rectangle([
(xcoord, ycoord),
(xcoord + BASE_SIZE, ycoord + BASE_SIZE)
], fill=BLACK, outline=None, width=1)
colour = warp['defaultColour']
if thread and thread.get('colour'):
colour = thread['colour']
draw.rectangle([
(xcoord, warp_top),
(xcoord + BASE_SIZE, warp_top + BASE_SIZE),
], fill=colour_tuple(colour))
# Draw weft
draw.rectangle([
(weft_left, weft_top),
(weft_right, weft_bottom)
], fill=None, outline=GREY, width=1)
for x in range(1, weft['treadles'] + 1):
xcoord = weft_left + x * BASE_SIZE
draw.line([
(xcoord, weft_top),
(xcoord, weft_bottom),
],
fill=GREY, width=1, joint=None)
for (i, y) in enumerate(range(0, len(weft['treadling']))):
thread = weft['treadling'][i]
ycoord = weft_top + y * BASE_SIZE
draw.line([
(weft_left, ycoord),
(weft_right, ycoord),
],
fill=GREY, width=1, joint=None)
if thread.get('treadle', 0) > 0:
xcoord = weft_left + (thread['treadle'] - 1) * BASE_SIZE
draw.rectangle([
(xcoord, ycoord),
(xcoord + BASE_SIZE, ycoord + BASE_SIZE)
], fill=BLACK, outline=None, width=1)
colour = weft['defaultColour']
if thread and thread.get('colour'):
colour = thread['colour']
draw.rectangle([
(weft_right - BASE_SIZE, ycoord),
(weft_right, ycoord + BASE_SIZE),
], fill=colour_tuple(colour))
# Draw tieups
draw.rectangle([
(tieup_left, tieup_top),
(tieup_right, tieup_bottom)
], fill=None, outline=GREY, width=1)
for y in range(1, warp['shafts'] + 1):
ycoord = y * BASE_SIZE
draw.line([
(tieup_left, ycoord),
(tieup_right, ycoord),
],
fill=GREY, width=1, joint=None)
for (x, tieup) in enumerate(tieups):
xcoord = tieup_left + x * BASE_SIZE
draw.line([
(xcoord, tieup_top),
(xcoord, tieup_bottom),
],
fill=GREY, width=1, joint=None)
for entry in tieup:
if entry > 0:
ycoord = tieup_bottom - (entry * BASE_SIZE)
draw.rectangle([
(xcoord, ycoord),
(xcoord + BASE_SIZE, ycoord + BASE_SIZE)
], fill=BLACK, outline=None, width=1)
# Draw drawdown
draw.rectangle([
(drawdown_left, drawdown_top),
(drawdown_right, drawdown_bottom)
], fill=None, outline=(0,0,0), width=1)
for (y, weft_thread) in enumerate(weft['treadling']):
for (x, warp_thread) in enumerate(warp['threading']):
treadle = 0 if weft_thread['treadle'] > weft['treadles'] else weft_thread['treadle']
shaft = warp_thread['shaft']
weft_colour = weft_thread.get('colour') or weft.get('defaultColour')
warp_colour = warp_thread.get('colour') or warp.get('defaultColour')
tieup = tieups[treadle-1] if treadle > 0 else []
tieup = [t for t in tieup if t < warp['shafts']]
thread_type = 'warp' if shaft in tieup else 'weft'
x1 = drawdown_right - (x + 1) * BASE_SIZE
x2 = drawdown_right - x * BASE_SIZE
y1 = drawdown_top + y * BASE_SIZE
y2 = drawdown_top + (y + 1) * BASE_SIZE
colour = colour_tuple(warp_colour if thread_type == 'warp' else weft_colour)
d = [0.6, 0.8, 0.9, 1.1, 1.3, 1.3, 1.1, 0.9, 0.8, 0.6, 0.5]
if thread_type == 'warp':
for (i, grad_x) in enumerate(range(x1, x2)):
draw.line([
(grad_x, y1), (grad_x, y2),
],
fill=(darken_colour(colour, d[i])), width=1, joint=None)
else:
for (i, grad_y) in enumerate(range(y1, y2)):
draw.line([
(x1, grad_y), (x2, grad_y),
],
fill=(darken_colour(colour, d[i])), width=1, joint=None)
in_mem_file = io.BytesIO()
img.save(in_mem_file, 'PNG')
in_mem_file.seek(0)
file_name = 'preview-{0}_{1}.png'.format(
'full' if with_plan else 'base', obj['_id']
)
path = 'projects/{}/{}'.format(obj['project'], file_name)
uploads.upload_file(path, in_mem_file)
return file_name

View File

@ -1,38 +1,4 @@
PODS:
- DKImagePickerController/Core (4.3.4):
- DKImagePickerController/ImageDataManager
- DKImagePickerController/Resource
- DKImagePickerController/ImageDataManager (4.3.4)
- DKImagePickerController/PhotoGallery (4.3.4):
- DKImagePickerController/Core
- DKPhotoGallery
- DKImagePickerController/Resource (4.3.4)
- DKPhotoGallery (0.0.17):
- DKPhotoGallery/Core (= 0.0.17)
- DKPhotoGallery/Model (= 0.0.17)
- DKPhotoGallery/Preview (= 0.0.17)
- DKPhotoGallery/Resource (= 0.0.17)
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Core (0.0.17):
- DKPhotoGallery/Model
- DKPhotoGallery/Preview
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Model (0.0.17):
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Preview (0.0.17):
- DKPhotoGallery/Model
- DKPhotoGallery/Resource
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Resource (0.0.17):
- SDWebImage
- SwiftyGif
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Firebase/CoreOnly (10.9.0):
- FirebaseCore (= 10.9.0)
- Firebase/Messaging (10.9.0):
@ -94,37 +60,23 @@ PODS:
- nanopb/encode (= 2.30909.0)
- nanopb/decode (2.30909.0)
- nanopb/encode (2.30909.0)
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- PromisesObjC (2.2.0)
- SDWebImage (5.18.8):
- SDWebImage/Core (= 5.18.8)
- SDWebImage/Core (5.18.8)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- SwiftyGif (5.4.4)
- url_launcher_ios (0.0.1):
- Flutter
DEPENDENCIES:
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- Flutter (from `Flutter`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
SPEC REPOS:
trunk:
- DKImagePickerController
- DKPhotoGallery
- Firebase
- FirebaseCore
- FirebaseCoreInternal
@ -134,12 +86,8 @@ SPEC REPOS:
- GoogleUtilities
- nanopb
- PromisesObjC
- SDWebImage
- SwiftyGif
EXTERNAL SOURCES:
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
firebase_core:
:path: ".symlinks/plugins/firebase_core/ios"
firebase_messaging:
@ -148,19 +96,12 @@ EXTERNAL SOURCES:
:path: Flutter
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
:path: ".symlinks/plugins/shared_preferences_foundation/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS:
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de
Firebase: bd152f0f3d278c4060c5c71359db08ebcfd5a3e2
firebase_core: ce64b0941c6d87c6ef5022ae9116a158236c8c94
firebase_messaging: 42912365e62efc1ea3e00724e5eecba6068ddb88
@ -173,14 +114,10 @@ SPEC CHECKSUMS:
GoogleUtilities: 9aa0ad5a7bc171f8bae016300bfcfa3fb8425749
image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef
SDWebImage: a81bbb3ba4ea5f810f4069c68727cb118467a04a
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
COCOAPODS: 1.14.2
COCOAPODS: 1.12.0

View File

@ -170,7 +170,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1430;
LastUpgradeCheck = 1300;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
@ -237,7 +237,6 @@
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (

View File

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

View File

@ -3,13 +3,12 @@ import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:io';
import 'package:shared_preferences/shared_preferences.dart';
import 'util.dart';
class Api {
String? _token;
//final String apiBase = 'https://api.treadl.com';
final String apiBase = 'http://192.168.5.134:2001';
final String apiBase = 'https://api.treadl.com';
//final String apiBase = 'http://192.168.5.86:2001';
Future<String?> loadToken() async {
if (_token != null) {
@ -103,18 +102,4 @@ class Api {
int status = response.statusCode;
return status == 200;
}
Future<File?> downloadFile(String url, String fileName) async {
Uri uri = Uri.parse(url);
http.Client client = http.Client();
http.Response response = await client.get(uri);
if(response.statusCode == 200) {
Util util = Util();
final String dirPath = await util.storagePath();
final file = File('$dirPath/$fileName');
await file.writeAsBytes(response.bodyBytes);
return file;
}
return null;
}
}

View File

@ -28,7 +28,7 @@ class _GroupsTabState extends State<GroupsTab> {
}
Widget buildGroupCard(Map<String,dynamic> group) {
String? description = group['description'];
String description = group['description'];
if (description != null && description.length > 80) {
description = description.substring(0, 77) + '...';
} else {
@ -44,11 +44,16 @@ class _GroupsTabState extends State<GroupsTab> {
),
);
},
child: ListTile(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new ListTile(
leading: Icon(Icons.people),
trailing: Icon(Icons.keyboard_arrow_right),
title: Text(group['name']),
subtitle: Text(description.replaceAll("\n", " ")),
),
]
)
)
)

View File

@ -3,60 +3,17 @@ import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_html/flutter_html.dart';
import 'dart:io';
import 'api.dart';
import 'util.dart';
import 'patterns/pattern.dart';
import 'patterns/viewer.dart';
class _ObjectScreenState extends State<ObjectScreen> {
final Map<String,dynamic> _project;
Map<String,dynamic> _object;
Map<String,dynamic>? _pattern;
bool _isLoading = false;
final Function _onUpdate;
final Function _onDelete;
final Api api = Api();
final Util util = Util();
_ObjectScreenState(this._object, this._project, this._onUpdate, this._onDelete) { }
@override
initState() {
super.initState();
if (_object['type'] == 'pattern') {
_fetchPattern();
}
}
void _fetchPattern() async {
var data = await api.request('GET', '/objects/' + _object['_id']);
if (data['success'] == true) {
setState(() {
_pattern = data['payload']['pattern'];
});
}
}
void _shareObject() async {
setState(() => _isLoading = true);
File? file;
if (_object['type'] == 'pattern') {
var data = await api.request('GET', '/objects/' + _object['_id'] + '/wif');
if (data['success'] == true) {
file = await util.writeFile(_object['name'] + '.wif', data['payload']['wif']);
}
} else {
String fileName = Uri.file(_object['url']).pathSegments.last;
file = await api.downloadFile(_object['url'], fileName);
}
if (file != null) {
util.shareFile(file!, withDelete: true);
}
setState(() => _isLoading = false);
}
void _deleteObject(BuildContext context, BuildContext modalContext) async {
var data = await api.request('DELETE', '/objects/' + _object['_id']);
if (data['success']) {
@ -111,6 +68,7 @@ class _ObjectScreenState extends State<ObjectScreen> {
TextButton(
child: Text('OK'),
onPressed: () async {
print(renameController.text);
var data = await api.request('PUT', '/objects/' + _object['_id'], {'name': renameController.text});
if (data['success']) {
Navigator.pop(context);
@ -160,10 +118,7 @@ class _ObjectScreenState extends State<ObjectScreen> {
return Image.network(_object['url']);
}
else if (_object['type'] == 'pattern') {
if (_pattern != null) {
return PatternViewer(_pattern!, withEditor: true);
}
else if (_object['previewUrl'] != null) {
if (_object['previewUrl'] != null) {
return Image.network(_object['previewUrl']!);;
}
else {
@ -178,16 +133,9 @@ class _ObjectScreenState extends State<ObjectScreen> {
}
}
else {
return Center(child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Treadl cannot display this type of item.'),
SizedBox(height: 20),
ElevatedButton(child: Text('View file'), onPressed: () {
return ElevatedButton(child: Text('View file'), onPressed: () {
launch(_object['url']);
}),
],
));
});
}
}
@ -200,12 +148,6 @@ class _ObjectScreenState extends State<ObjectScreen> {
appBar: AppBar(
title: Text(_object['name']),
actions: <Widget>[
IconButton(
icon: Icon(Icons.ios_share),
onPressed: () {
_shareObject();
},
),
IconButton(
icon: Icon(Icons.settings),
onPressed: () {
@ -216,10 +158,10 @@ class _ObjectScreenState extends State<ObjectScreen> {
),
body: Container(
margin: const EdgeInsets.all(10.0),
child: Column(
children: [
_isLoading ? LinearProgressIndicator() : SizedBox(height: 0),
Expanded(child: getObjectWidget()),
child: ListView(
children: <Widget>[
getObjectWidget(),
Html(data: description)
]
)
),
@ -236,4 +178,3 @@ class ObjectScreen extends StatefulWidget {
@override
_ObjectScreenState createState() => _ObjectScreenState(_object, _project, _onUpdate, _onDelete);
}

View File

@ -1,76 +0,0 @@
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
import '../util.dart';
class DrawdownPainter extends CustomPainter {
final Map<String,dynamic> pattern;
final double BASE_SIZE;
final Util util = Util();
@override
DrawdownPainter(this.BASE_SIZE, this.pattern) {}
@override
void paint(Canvas canvas, Size size) {
var weft = pattern['weft'];
var warp = pattern['warp'];
var tieups = pattern['tieups'];
var paint = Paint()
..color = Colors.black
..strokeWidth = 1;
// Draw grid
for (double i = 0; i <= size.width; i += BASE_SIZE) {
canvas.drawLine(Offset(i.toDouble(), size.height), Offset(i.toDouble(), 0), paint);
}
for (double y = 0; y <= size.height; y += BASE_SIZE) {
canvas.drawLine(Offset(0, y.toDouble()), Offset(size.width, y.toDouble()), paint);
}
for (int tread = 0; tread < weft['treadling']?.length; tread++) {
for (int thread = 0; thread < warp['threading']?.length; thread++) {
// Ensure we only get a treadle in the allowed bounds
int treadle = weft['treadling'][tread]['treadle'] > weft['treadles'] ? 0 : weft['treadling'][tread]['treadle'];
int shaft = warp['threading'][thread]['shaft'];
Color weftColour = util.rgb(weft['treadling'][tread]['colour'] ?? weft['defaultColour']);
Color warpColour = util.rgb(warp['threading'][thread]['colour'] ?? warp['defaultColour']);
// Only capture valid tie-ups (e.g. in case there is data for more shafts, which are then reduced)
// Dart throws error if index < 0 so check fiest
List<dynamic> tieup = treadle > 0 ? tieups[treadle - 1] : [];
List<dynamic> filteredTieup = tieup.where((t) => t< warp['shafts']).toList();
String threadType = filteredTieup.contains(shaft) ? 'warp' : 'weft';
Rect rect = Offset(
size.width - BASE_SIZE * (thread + 1),
tread * BASE_SIZE
) & Size(BASE_SIZE, BASE_SIZE);
canvas.drawRect(
rect,
Paint()
..color = threadType == 'warp' ? warpColour : weftColour
);
canvas.drawRect(
rect,
Paint()
..shader = ui.Gradient.linear(
threadType == 'warp' ? rect.centerLeft : rect.topCenter,
threadType == 'warp' ? rect.centerRight : rect.bottomCenter,
[
Color.fromRGBO(0,0,0,0.4),
Color.fromRGBO(0,0,0,0.0),
Color.fromRGBO(0,0,0,0.4),
],
[0.0,0.5,1.0],
)
);
}
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}

View File

@ -1,102 +0,0 @@
import 'package:flutter/material.dart';
import 'warp.dart';
import 'weft.dart';
import 'tieup.dart';
import 'drawdown.dart';
class Pattern extends StatelessWidget {
final Map<String,dynamic> pattern;
final Function? onUpdate;
final double BASE_SIZE = 5;
@override
Pattern(this.pattern, {this.onUpdate}) {}
@override
Widget build(BuildContext context) {
var warp = pattern['warp'];
var weft = pattern['weft'];
double draftWidth = warp['threading']?.length * BASE_SIZE + weft['treadles'] * BASE_SIZE + BASE_SIZE;
double draftHeight = warp['shafts'] * BASE_SIZE + weft['treadling']?.length * BASE_SIZE + BASE_SIZE;
double tieupTop = BASE_SIZE;
double tieupRight = BASE_SIZE;
double tieupWidth = weft['treadles'] * BASE_SIZE;
double tieupHeight = warp['shafts'] * BASE_SIZE;
double warpTop = 0;
double warpRight = weft['treadles'] * BASE_SIZE + BASE_SIZE * 2;
double warpWidth = warp['threading']?.length * BASE_SIZE;
double warpHeight = warp['shafts'] * BASE_SIZE + BASE_SIZE;
double weftRight = 0;
double weftTop = warp['shafts'] * BASE_SIZE + BASE_SIZE * 2;
double weftWidth = weft['treadles'] * BASE_SIZE + BASE_SIZE;
double weftHeight = weft['treadling'].length * BASE_SIZE;
double drawdownTop = warpHeight + BASE_SIZE;
double drawdownRight = weftWidth + BASE_SIZE;
double drawdownWidth = warpWidth;
double drawdownHeight = weftHeight;
return Container(
width: draftWidth,
height: draftHeight,
child: Stack(
children: [
Positioned(
right: tieupRight,
top: tieupTop,
child: GestureDetector(
onTapDown: (details) {
var tieups = pattern['tieups'];
double dx = details.localPosition.dx;
double dy = details.localPosition.dy;
int tie = (dx / BASE_SIZE).toInt();
int shaft = ((tieupHeight - dy) / BASE_SIZE).toInt() + 1;
if (tieups[tie].contains(shaft)) {
tieups[tie].remove(shaft);
} else {
tieups[tie].add(shaft);
}
print(tieups);
if (onUpdate != null) {
onUpdate!({'tieups': tieups});
}
// Toggle tieups[tie][shaft]
},
child: CustomPaint(
size: Size(tieupWidth, tieupHeight),
painter: TieupPainter(BASE_SIZE, this.pattern),
)),
),
Positioned(
right: warpRight,
top: warpTop,
child: CustomPaint(
size: Size(warpWidth, warpHeight),
painter: WarpPainter(BASE_SIZE, this.pattern),
),
),
Positioned(
right: weftRight,
top: weftTop,
child: CustomPaint(
size: Size(weftWidth, weftHeight),
painter: WeftPainter(BASE_SIZE, this.pattern),
),
),
Positioned(
right: drawdownRight,
top: drawdownTop,
child: CustomPaint(
size: Size(drawdownWidth, drawdownHeight),
painter: DrawdownPainter(BASE_SIZE, this.pattern),
),
)
]
)
);
}
}

View File

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

View File

@ -1,68 +0,0 @@
import 'package:flutter/material.dart';
import 'pattern.dart';
class PatternViewer extends StatefulWidget {
final Map<String,dynamic> pattern;
final bool withEditor;
PatternViewer(this.pattern, {this.withEditor = false}) {}
@override
State<PatternViewer> createState() => _PatternViewerState(this.pattern, this.withEditor);
}
class _PatternViewerState extends State<PatternViewer> {
Map<String,dynamic> pattern;
final bool withEditor;
bool controllerInitialised = false;
final controller = TransformationController();
final double BASE_SIZE = 5;
_PatternViewerState(this.pattern, this.withEditor) {}
void updatePattern(update) {
setState(() {
pattern!.addAll(update);
});
}
@override
Widget build(BuildContext context) {
if (!controllerInitialised) {
var warp = pattern['warp'];
var weft = pattern['weft'];
double draftWidth = warp['threading']?.length * BASE_SIZE + weft['treadles'] * BASE_SIZE + BASE_SIZE;
final zoomFactor = 1.0;
final xTranslate = draftWidth - MediaQuery.of(context).size.width - 0;
final yTranslate = 0.0;
controller.value.setEntry(0, 0, zoomFactor);
controller.value.setEntry(1, 1, zoomFactor);
controller.value.setEntry(2, 2, zoomFactor);
controller.value.setEntry(0, 3, -xTranslate);
controller.value.setEntry(1, 3, -yTranslate);
setState(() => controllerInitialised = true);
}
return InteractiveViewer(
minScale: 0.5,
maxScale: 5,
constrained: false,
transformationController: controller,
child: RepaintBoundary(child: Pattern(pattern))
);
/*return Column(
children: [
Text('Hi'),
Expanded(child: InteractiveViewer(
minScale: 0.5,
maxScale: 5,
constrained: false,
transformationController: controller,
child: RepaintBoundary(child: Pattern(pattern))))
,
Text('Another'),
]
);*/
}
}

View File

@ -1,66 +0,0 @@
import 'package:flutter/material.dart';
import '../util.dart';
class WarpPainter extends CustomPainter {
final Map<String,dynamic> pattern;
final double BASE_SIZE;
final Util util = Util();
@override
WarpPainter(this.BASE_SIZE, this.pattern) {}
@override
void paint(Canvas canvas, Size size) {
var warp = pattern['warp'];
var paint = Paint()
..color = Colors.black
..strokeWidth = 0.5;
var thickPaint = Paint()
..color = Colors.black
..strokeWidth = 1.5;
// Draw grid
int columnsPainted = 0;
for (double i = size.width; i >= 0; i -= BASE_SIZE) {
canvas.drawLine(Offset(i.toDouble(), size.height), Offset(i.toDouble(), 0), paint);
columnsPainted += 1;
}
for (double y = 0; y <= size.height; y += BASE_SIZE) {
canvas.drawLine(Offset(0, y.toDouble()), Offset(size.width, y.toDouble()), paint);
}
// Draw threads
for (var i = 0; i < warp['threading'].length; i++) {
var thread = warp['threading'][i];
int? shaft = thread?['shaft'];
String? colour = warp['defaultColour'];
double x = size.width - (i+1)*BASE_SIZE;
if (shaft != null) {
if (shaft! > 0) {
canvas.drawRect(
Offset(x, size.height - shaft!*BASE_SIZE) &
Size(BASE_SIZE.toDouble(), BASE_SIZE.toDouble()),
paint
);
}
}
if (thread?['colour'] != null) {
colour = thread!['colour'];
}
if (colour != null) {
canvas.drawRect(
Offset(x, 0) &
Size(BASE_SIZE.toDouble(), BASE_SIZE.toDouble()),
Paint()
..color = util.rgb(colour!)
);
}
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}

View File

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

View File

@ -2,12 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import 'package:image_picker/image_picker.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
import 'package:intl/intl.dart';
import 'dart:io';
import 'api.dart';
import 'util.dart';
import 'object.dart';
class _ProjectScreenState extends State<ProjectScreen> {
@ -15,7 +11,6 @@ class _ProjectScreenState extends State<ProjectScreen> {
final Function _onDelete;
final picker = ImagePicker();
final Api api = Api();
final Util util = Util();
Map<String,dynamic> _project;
List<dynamic> _objects = [];
bool _loading = false;
@ -31,6 +26,7 @@ class _ProjectScreenState extends State<ProjectScreen> {
void getObjects(String fullName) async {
setState(() => _loading = true);
print(fullName);
var data = await api.request('GET', '/projects/' + fullName + '/objects');
if (data['success'] == true) {
setState(() {
@ -40,10 +36,6 @@ class _ProjectScreenState extends State<ProjectScreen> {
}
}
void _shareProject() {
util.shareUrl('Check out my project on Treadl', util.appUrl(_project['fullName']));
}
void _onDeleteProject() {
Navigator.pop(context);
_onDelete(_project['_id']);
@ -73,74 +65,12 @@ class _ProjectScreenState extends State<ProjectScreen> {
});
}
void _createObject(objectData) async {
String fullPath = _project['fullName'];
var resp = await api.request('POST', '/projects/$fullPath/objects', objectData);
setState(() => _creating = false);
if (resp['success']) {
List<dynamic> newObjects = _objects;
newObjects.add(resp['payload']);
setState(() {
_objects = newObjects;
});
}
}
void _createObjectFromWif(String name, String wif) {
setState(() => _creating = true);
_createObject({
'name': name,
'type': 'pattern',
'wif': wif,
});
}
void _createObjectFromFile(String name, XFile file) async {
final int size = await file.length();
final String forId = _project['_id'];
final String type = file.mimeType ?? 'text/plain';
setState(() => _creating = true);
var data = await api.request('GET', '/uploads/file/request?name=$name&size=$size&type=$type&forType=project&forId=$forId');
if (!data['success']) {
setState(() => _creating = false);
return;
}
var uploadSuccess = await api.putFile(data['payload']['signedRequest'], File(file.path), type);
if (!uploadSuccess) {
setState(() => _creating = false);
return;
}
_createObject({
'name': name,
'storedName': data['payload']['fileName'],
'type': 'file',
});
}
void _chooseFile() async {
FilePickerResult? result = await FilePicker.platform.pickFiles();
if (result != null) {
PlatformFile file = result.files.single;
XFile xFile = XFile(file.path!);
String? ext = file.extension;
if (ext != null && ext!.toLowerCase() == 'wif' || xFile.name.toLowerCase().contains('.wif')) {
final String contents = await xFile.readAsString();
_createObjectFromWif(file.name, contents);
} else {
_createObjectFromFile(file.name, xFile);
}
}
}
void _chooseImage() async {
File file;
try {
final XFile? imageFile = await picker.pickImage(source: ImageSource.gallery);
final imageFile = await picker.getImage(source: ImageSource.gallery);
if (imageFile == null) return;
final f = new DateFormat('yyyy-MM-dd_hh-mm-ss');
String time = f.format(new DateTime.now());
String name = _project['name'] + ' ' + time + '.' + imageFile.name.split('.').last;
_createObjectFromFile(name, imageFile);
file = File(imageFile.path);
}
on Exception {
showDialog(
@ -157,6 +87,41 @@ class _ProjectScreenState extends State<ProjectScreen> {
],
)
);
return;
}
final int size = await file.length();
final String forId = _project['_id'];
final String fullPath = _project['owner']['username'] + '/' + _project['path'];
final String name = file.path.split('/').last;
final String ext = name.split('.').last;
final String type = 'image/jpeg';//$ext';
setState(() => _creating = true);
var data = await api.request('GET', '/uploads/file/request?name=$name&size=$size&type=$type&forType=project&forId=$forId');
print(data);
if (!data['success']) {
setState(() => _creating = false);
return;
}
var uploadSuccess = await api.putFile(data['payload']['signedRequest'], file, type);
print(uploadSuccess);
if (!uploadSuccess) {
setState(() => _creating = false);
return;
}
var newObjectData = {
'name': name,
'storedName': data['payload']['fileName'],
'type': 'file',
};
var objectData = await api.request('POST', '/projects/$fullPath/objects', newObjectData);
setState(() => _creating = false);
if (objectData['success']) {
List<dynamic> newObjects = _objects;
newObjects.add(objectData['payload']);
setState(() {
_objects = newObjects;
});
}
}
@ -165,14 +130,27 @@ class _ProjectScreenState extends State<ProjectScreen> {
showCupertinoModalPopup(context: context, builder: (BuildContext context) => settingsDialog);
}
Widget getMemoryImageBox(data, [bool? isMemory, bool? isNetwork]) {
return new AspectRatio(
aspectRatio: 1 / 1,
child: new Container(
decoration: new BoxDecoration(
image: new DecorationImage(
fit: BoxFit.fitWidth,
alignment: FractionalOffset.topCenter,
image: new MemoryImage(data),
)
),
),
);
}
Widget getNetworkImageBox(String url) {
return new AspectRatio(
aspectRatio: 1 / 1,
child: new Container(
decoration: new BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
image: new DecorationImage(
fit: BoxFit.cover,
fit: BoxFit.fitWidth,
alignment: FractionalOffset.topCenter,
image: new NetworkImage(url),
)
@ -183,13 +161,7 @@ class _ProjectScreenState extends State<ProjectScreen> {
Widget getIconBox(Icon icon) {
return new AspectRatio(
aspectRatio: 1 / 1,
child: new Container(
decoration: new BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(10.0),
),
child: icon
),
);
}
@ -243,12 +215,17 @@ class _ProjectScreenState extends State<ProjectScreen> {
),
);
},
child: ListTile(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new ListTile(
leading: leader,
trailing: Icon(Icons.keyboard_arrow_right),
title: Text(object['name']),
subtitle: Text(type),
),
]
)
)
);
}
@ -259,12 +236,6 @@ class _ProjectScreenState extends State<ProjectScreen> {
appBar: AppBar(
title: Text(_project['name']),
actions: <Widget>[
IconButton(
icon: Icon(Icons.ios_share),
onPressed: () {
_shareProject();
},
),
IconButton(
icon: Icon(Icons.settings),
onPressed: () {
@ -295,50 +266,13 @@ class _ProjectScreenState extends State<ProjectScreen> {
children: [
Text('This project is currently empty', style: TextStyle(fontSize: 20), textAlign: TextAlign.center),
Image(image: AssetImage('assets/empty.png'), width: 300),
Text('Add a pattern file, an image, or something else to this project using the + button below.', textAlign: TextAlign.center),
Text('Add something to this project using the button below.', textAlign: TextAlign.center),
])
),
floatingActionButtonLocation: ExpandableFab.location,
floatingActionButton: ExpandableFab(
distance: 70,
type: ExpandableFabType.up,
openButtonBuilder: RotateFloatingActionButtonBuilder(
child: const Icon(Icons.add),
),
children: [
Row(children:[
Container(
padding: EdgeInsets.all(5),
decoration: BoxDecoration(
color: Colors.grey[800],
borderRadius: BorderRadius.all(Radius.circular(12)),
),
child: Text('Add an image', style: TextStyle(fontSize: 15, color: Colors.white)),
),
SizedBox(width: 10),
FloatingActionButton(
heroTag: null,
floatingActionButton: FloatingActionButton(
onPressed: _chooseImage,
child: Icon(Icons.image_outlined),
),
]),
Row(children:[
Container(
padding: EdgeInsets.all(5),
decoration: BoxDecoration(
color: Colors.grey[800],
borderRadius: BorderRadius.all(Radius.circular(12)),
),
child: Text('Add a WIF or other file', style: TextStyle(fontSize: 15, color: Colors.white)),
),
SizedBox(width: 10),
FloatingActionButton(
heroTag: null,
child: const Icon(Icons.insert_drive_file_outlined),
onPressed: _chooseFile,
),
]),
],
child: Icon(Icons.cloud_upload),
backgroundColor: Colors.pink[500],
),
);
}
@ -382,6 +316,7 @@ class _ProjectSettingsDialog extends StatelessWidget {
TextButton(
child: Text('OK'),
onPressed: () async {
print(renameController.text);
var data = await api.request('PUT', '/projects/' + _project['owner']['username'] + '/' + _project['path'], {'name': renameController.text});
if (data['success']) {
Navigator.pop(context);

View File

@ -88,24 +88,19 @@ class _ProjectsTabState extends State<ProjectsTab> {
),
);
},
child: Container(
padding: EdgeInsets.all(5),
child: ListTile(
leading: new AspectRatio(
aspectRatio: 1 / 1,
child: new Container(
decoration: new BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(10.0),
),
child: Icon(Icons.folder)
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
new ListTile(
leading: Icon(Icons.folder_open),
trailing: Icon(Icons.keyboard_arrow_right),
title: Text(project['name'] != null ? project['name'] : 'Untitled project'),
subtitle: Text(description),
),
))
]
),
)
)
;
}

View File

@ -41,14 +41,6 @@ class _UserScreenState extends State<UserScreen> {
return Scaffold(
appBar: AppBar(
title: Text(_user['username']),
actions: <Widget>[
IconButton(
icon: Icon(Icons.person),
onPressed: () {
launch('https://www.treadl.com/' + _user['username']);
},
),
]
),
body: _loading ?
Container(
@ -58,7 +50,6 @@ class _UserScreenState extends State<UserScreen> {
)
: Container(
padding: EdgeInsets.all(10),
margin: EdgeInsets.only(top: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@ -69,12 +60,11 @@ class _UserScreenState extends State<UserScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(_user['username'], style: Theme.of(context).textTheme.titleLarge),
Text(_user['username'], style: Theme.of(context).textTheme.titleMedium),
SizedBox(height: 5),
_user['location'] != null ?
Row(children: [
Icon(CupertinoIcons.location),
SizedBox(width: 10),
Text(_user['location'])
]) : SizedBox(height: 1),
SizedBox(height: 10),

View File

@ -1,28 +1,21 @@
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'dart:io';
import 'dart:convert';
import 'api.dart';
String APP_URL = 'https://www.treadl.com';
class Util {
ImageProvider? avatarUrl(Map<String,dynamic> user) {
ImageProvider avatarUrl(Map<String,dynamic> user) {
ImageProvider a = AssetImage('assets/avatars/9.png');
if (user != null && user['avatar'] != null) {
if (user['avatar'].length < 3) {
return AssetImage('assets/avatars/${user['avatar']}.png');
a = AssetImage('assets/avatars/${user['avatar']}.png');
}
else {
return NetworkImage(user['avatarUrl']);
a =NetworkImage(user['avatarUrl']);
}
}
return null;
return a;
}
Widget avatarImage(ImageProvider? image, {double size=30}) {
if (image != null) {
Widget avatarImage(ImageProvider image, {double size=30}) {
return new Container(
width: size,
height: size,
@ -35,49 +28,4 @@ class Util {
)
);
}
return new Container(
width: size,
height: size,
child: Icon(Icons.person)
);
}
Color rgb(String input) {
List<String> parts = input.split(',');
List<int> iParts = parts.map((p) => int.parse(p)).toList();
iParts = iParts.map((p) => p > 255 ? 255 : p).toList();
return Color.fromRGBO(iParts[0], iParts[1], iParts[2], 1);
}
String appUrl(String path) {
return APP_URL + '/' + path;
}
Future<String> storagePath() async {
final Directory directory = await getApplicationDocumentsDirectory();
return directory.path;
}
Future<File> writeFile(String fileName, String data) async {
final String dirPath = await storagePath();
final file = File('$dirPath/$fileName');
String contents = data.replaceAll(RegExp(r'\\n'), '\r\n');
return await file.writeAsString(contents);
}
Future<bool> deleteFile(File file) async {
await file.delete();
return true;
}
void shareFile(File file, {bool? withDelete}) async {
await Share.shareXFiles([XFile(file.path)]);
if (withDelete == true) {
await deleteFile(file);
}
}
void shareUrl(String text, String url) async {
await Share.share('$text: $url');
}
}

View File

@ -29,10 +29,10 @@ packages:
dependency: transitive
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0
url: "https://pub.dev"
source: hosted
version: "2.11.0"
version: "2.10.0"
boolean_selector:
dependency: transitive
description:
@ -45,10 +45,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.2.1"
clock:
dependency: transitive
description:
@ -61,10 +61,10 @@ packages:
dependency: transitive
description:
name: collection
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0
url: "https://pub.dev"
source: hosted
version: "1.17.2"
version: "1.17.0"
convert:
dependency: transitive
description:
@ -129,14 +129,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.4"
file_picker:
dependency: "direct main"
description:
name: file_picker
sha256: "4e42aacde3b993c5947467ab640882c56947d9d27342a5b6f2895b23956954a6"
url: "https://pub.dev"
source: hosted
version: "6.1.1"
file_selector_linux:
dependency: transitive
description:
@ -222,14 +214,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_expandable_fab:
dependency: "direct main"
description:
name: flutter_expandable_fab
sha256: "2aa5735bebcdbc49f43bcb32a29f9f03a9b7029212b8cd9837ae332ab2edf647"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
flutter_html:
dependency: "direct main"
description:
@ -300,50 +284,50 @@ packages:
dependency: "direct main"
description:
name: image_picker
sha256: "340efe08645537d6b088a30620ee5752298b1630f23a829181172610b868262b"
sha256: "6432178560d95303cc70d038363f892f5a05750dd27bc55220c7301af54d05e9"
url: "https://pub.dev"
source: hosted
version: "1.0.6"
version: "0.8.8"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
sha256: "1a27bf4cc0330389cebe465bab08fe6dec97e44015b4899637344bb7297759ec"
sha256: "1ec6830289f5b6aeff3aa8239ea737c71950178dda389342dc2215adb06b4bd8"
url: "https://pub.dev"
source: hosted
version: "0.8.9+2"
version: "0.8.6+20"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
sha256: "869fe8a64771b7afbc99fc433a5f7be2fea4d1cb3d7c11a48b6b579eb9c797f0"
sha256: "98f50d6b9f294c8ba35e25cc0d13b04bfddd25dbc8d32fa9d566a6572f2c081c"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.1.12"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
sha256: eac0a62104fa12feed213596df0321f57ce5a572562f72a68c4ff81e9e4caacf
sha256: d779210bda268a03b57e923fb1e410f32f5c5e708ad256348bcbf1f44f558fd0
url: "https://pub.dev"
source: hosted
version: "0.8.9"
version: "0.8.7+4"
image_picker_linux:
dependency: transitive
description:
name: image_picker_linux
sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa"
sha256: "1d8f9a97178d6b8a035f1d2765f17f8ca3d36a40d5594e742a481b1e002f20be"
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
version: "0.2.0"
image_picker_macos:
dependency: transitive
description:
name: image_picker_macos
sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62"
sha256: ff094b36d6c06200808f733144a033e45b4e17d59524e1cf7d2af7e4cb94e1ab
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
version: "0.2.0"
image_picker_platform_interface:
dependency: transitive
description:
@ -356,10 +340,10 @@ packages:
dependency: transitive
description:
name: image_picker_windows
sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
sha256: bf77b819eb62c487e6af53b9eb213adc12bd060ef7e43f3b1dd69c53cc24a61d
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
version: "0.2.0"
intl:
dependency: "direct main"
description:
@ -388,34 +372,26 @@ packages:
dependency: transitive
description:
name: matcher
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72"
url: "https://pub.dev"
source: hosted
version: "0.12.16"
version: "0.12.13"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
url: "https://pub.dev"
source: hosted
version: "0.5.0"
version: "0.2.0"
meta:
dependency: transitive
description:
name: meta
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
mime:
dependency: transitive
description:
name: mime
sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e
url: "https://pub.dev"
source: hosted
version: "1.0.4"
version: "1.8.0"
nested:
dependency: transitive
description:
@ -428,58 +404,34 @@ packages:
dependency: transitive
description:
name: path
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b
url: "https://pub.dev"
source: hosted
version: "1.8.3"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa
url: "https://pub.dev"
source: hosted
version: "2.1.1"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
version: "1.8.2"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.1.11"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c"
sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.0.6"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.1.7"
petitparser:
dependency: transitive
description:
@ -528,22 +480,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.5"
share_plus:
dependency: "direct main"
description:
name: share_plus
sha256: f74fc3f1cbd99f39760182e176802f693fa0ec9625c045561cfad54681ea93dd
url: "https://pub.dev"
source: hosted
version: "7.2.1"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956
url: "https://pub.dev"
source: hosted
version: "3.3.1"
shared_preferences:
dependency: "direct main"
description:
@ -609,18 +545,10 @@ packages:
dependency: transitive
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
url: "https://pub.dev"
source: hosted
version: "1.10.0"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
version: "1.9.1"
stack_trace:
dependency: transitive
description:
@ -657,10 +585,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206
url: "https://pub.dev"
source: hosted
version: "0.6.0"
version: "0.4.16"
typed_data:
dependency: transitive
description:
@ -733,14 +661,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.6"
uuid:
dependency: transitive
description:
name: uuid
sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f"
url: "https://pub.dev"
source: hosted
version: "4.2.2"
vector_math:
dependency: transitive
description:
@ -749,22 +669,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
web:
dependency: transitive
description:
name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
url: "https://pub.dev"
source: hosted
version: "0.1.4-beta"
win32:
dependency: transitive
description:
name: win32
sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0
sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c"
url: "https://pub.dev"
source: hosted
version: "5.0.6"
version: "4.1.4"
xdg_directories:
dependency: transitive
description:
@ -790,5 +702,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.1.0-185.0.dev <4.0.0"
flutter: ">=3.10.0"
dart: ">=2.19.0 <3.0.0"
flutter: ">=3.3.0"

View File

@ -30,14 +30,9 @@ dependencies:
url_launcher: ^6.1.2
flutter_html: ^3.0.0-alpha.3
intl: ^0.17.0
image_picker: ^1.0.6
file_picker: ^6.1.1
image_picker: ^0.8.5+3
flutter_launcher_icons: ^0.9.0
firebase_messaging: ^14.4.0
path_provider: ^2.1.1
share_plus: ^7.2.1
flutter_expandable_fab: ^2.0.0
#fluttertoast: ^8.0.9
dev_dependencies:

View File

@ -114,7 +114,7 @@ export default function NavBar() {
<Menu.Menu position='right'>
{isAuthenticated && <>
<Menu.Item className='abovee-mobile'><SearchBar /></Menu.Item>
<Dropdown direction="left" pointing="top right" icon={null} style={{ marginTop: 10, zIndex: 30}}
<Dropdown direction="left" pointing="top right" icon={null} style={{ marginTop: 10}}
trigger={<UserChip user={user} withoutLink avatarOnly />}
>
<Dropdown.Menu style={{ minWidth: '200px', paddingTop: 10 }}>

View File

@ -111,7 +111,7 @@ function ObjectViewer() {
}
{(utils.canEditProject(user, project) || project.openSource) &&
<React.Fragment>
{(object.fullPreviewUrl || object.patternImage) &&
{object.patternImage &&
<Dropdown.Item icon='file outline' content='Download complete pattern as an image' onClick={e => utils.downloadPatternImage(object)}/>
}
<Dropdown.Divider />

View File

@ -43,6 +43,7 @@ function Draft() {
dispatch(actions.objects.receive(o));
setObject(o);
setPattern(o.pattern);
setTimeout(() => generatePreviews(o), 1000);
});
}, [objectId]);
@ -62,10 +63,20 @@ function Draft() {
setUnsaved(true);
};
const generatePreviews = (o) => {
util.generatePatternPreview(o, previewUrl => {
util.generateCompletePattern(o?.pattern, ``, patternImage => {
setObject(Object.assign({}, o, { previewUrl, patternImage }));
});
});
}
const saveObject = () => {
setSaving(true);
generatePreviews();
const newObject = Object.assign({}, object);
newObject.pattern = pattern;
generatePreviews(newObject);
api.objects.update(objectId, newObject, (o) => {
toast.success('Pattern saved');
dispatch(actions.objects.receive(o));

View File

@ -34,12 +34,10 @@ function DraftPreview({ object }) {
dispatch(actions.objects.update(objectId, 'previewUrl', previewUrl));
});
}
if (!o.fullPreviewUrl) {
// Generate the entire pattern and store in memory
util.generateCompletePattern(o.pattern, `.preview-${objectId}`, image => {
dispatch(actions.objects.update(objectId, 'patternImage', image));
});
}
}, 1000);
}
}, err => setLoading(false));

View File

@ -135,7 +135,7 @@ const utils = {
downloadPatternImage(object) {
const element = document.createElement('a');
element.setAttribute('target', '_blank');
element.setAttribute('href', object.fullPreviewUrl || object.patternImage);
element.setAttribute('href', object.patternImage);
element.setAttribute('download', `${object.name.replace(/ /g, '_')}-pattern.png`);
element.style.display = 'none';
document.body.appendChild(element);