Compare commits
32 Commits
46ed954e53
...
6e15952ffc
Author | SHA1 | Date | |
---|---|---|---|
6e15952ffc | |||
20b94e553d | |||
572d39e947 | |||
dcf44f6b1d | |||
4b410ec31e | |||
f63866a04c | |||
104879ee27 | |||
f94769e228 | |||
88c0b44444 | |||
3403134072 | |||
e6178c8a72 | |||
58bf8ca74e | |||
6cfcf0c5a1 | |||
65b379f162 | |||
4bf03c7c67 | |||
aeb60dd840 | |||
3b2c1e7f4c | |||
a2cde7de81 | |||
b14f438597 | |||
ac97481e6e | |||
1129a9df48 | |||
afc32578cf | |||
f44d56182b | |||
14f930af13 | |||
9ed84493cc | |||
ee11984a00 | |||
dcb9453ccd | |||
45725f52c1 | |||
5e108cf8c8 | |||
1406f9c4c8 | |||
3e79d950b1 | |||
ea792bd75d |
@ -32,6 +32,8 @@ 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):
|
||||
@ -54,6 +56,9 @@ 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
|
||||
|
||||
@ -100,7 +105,15 @@ 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', 'preview']
|
||||
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']
|
||||
|
||||
updater = util.build_updater(data, allowed_keys)
|
||||
if updater:
|
||||
db.objects.update({'_id': ObjectId(id)}, updater)
|
||||
|
@ -1,7 +1,7 @@
|
||||
import datetime, re
|
||||
from bson.objectid import ObjectId
|
||||
from util import database, wif, util
|
||||
from api import uploads
|
||||
from api import uploads, objects
|
||||
|
||||
default_pattern = {
|
||||
'warp': {
|
||||
@ -118,13 +118,15 @@ 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, '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, 'fullPreview': 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):
|
||||
@ -156,36 +158,32 @@ 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 = {
|
||||
'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
|
||||
obj['name'] = pattern['name']
|
||||
obj['pattern'] = pattern
|
||||
except Exception as e:
|
||||
raise util.errors.BadRequest('Unable to load WIF file. It is either invalid or in a format we cannot understand.')
|
||||
elif data.get('name'):
|
||||
else:
|
||||
pattern = default_pattern.copy()
|
||||
pattern['warp'].update({'shafts': data.get('shafts', 8)})
|
||||
pattern['weft'].update({'treadles': data.get('treadles', 8)})
|
||||
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
|
||||
return obj
|
||||
obj['name'] = data.get('name') or 'Untitled Pattern'
|
||||
obj['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'])
|
||||
raise util.errors.BadRequest('Unable to create object')
|
||||
|
||||
|
||||
|
@ -32,6 +32,14 @@ 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
1663
api/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -22,6 +22,7 @@ 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]
|
||||
|
||||
|
202
api/util/wif.py
202
api/util/wif.py
@ -1,4 +1,7 @@
|
||||
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
|
||||
@ -16,6 +19,19 @@ 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
|
||||
@ -197,3 +213,189 @@ 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
|
||||
|
@ -1,4 +1,38 @@
|
||||
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):
|
||||
@ -60,23 +94,37 @@ 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`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/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`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- DKImagePickerController
|
||||
- DKPhotoGallery
|
||||
- Firebase
|
||||
- FirebaseCore
|
||||
- FirebaseCoreInternal
|
||||
@ -86,8 +134,12 @@ 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:
|
||||
@ -96,12 +148,19 @@ 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/ios"
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
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
|
||||
@ -114,10 +173,14 @@ 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.12.0
|
||||
COCOAPODS: 1.14.2
|
||||
|
@ -170,7 +170,7 @@
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1300;
|
||||
LastUpgradeCheck = 1430;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
@ -237,6 +237,7 @@
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1300"
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -3,12 +3,13 @@ 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.86:2001';
|
||||
//final String apiBase = 'https://api.treadl.com';
|
||||
final String apiBase = 'http://192.168.5.134:2001';
|
||||
|
||||
Future<String?> loadToken() async {
|
||||
if (_token != null) {
|
||||
@ -102,4 +103,18 @@ 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;
|
||||
}
|
||||
}
|
||||
|
@ -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,16 +44,11 @@ class _GroupsTabState extends State<GroupsTab> {
|
||||
),
|
||||
);
|
||||
},
|
||||
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", " ")),
|
||||
),
|
||||
]
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.people),
|
||||
trailing: Icon(Icons.keyboard_arrow_right),
|
||||
title: Text(group['name']),
|
||||
subtitle: Text(description.replaceAll("\n", " ")),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -3,17 +3,60 @@ 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']) {
|
||||
@ -68,7 +111,6 @@ 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);
|
||||
@ -118,7 +160,10 @@ class _ObjectScreenState extends State<ObjectScreen> {
|
||||
return Image.network(_object['url']);
|
||||
}
|
||||
else if (_object['type'] == 'pattern') {
|
||||
if (_object['previewUrl'] != null) {
|
||||
if (_pattern != null) {
|
||||
return PatternViewer(_pattern!, withEditor: true);
|
||||
}
|
||||
else if (_object['previewUrl'] != null) {
|
||||
return Image.network(_object['previewUrl']!);;
|
||||
}
|
||||
else {
|
||||
@ -133,9 +178,16 @@ class _ObjectScreenState extends State<ObjectScreen> {
|
||||
}
|
||||
}
|
||||
else {
|
||||
return ElevatedButton(child: Text('View file'), onPressed: () {
|
||||
launch(_object['url']);
|
||||
});
|
||||
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: () {
|
||||
launch(_object['url']);
|
||||
}),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,6 +200,12 @@ 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: () {
|
||||
@ -158,10 +216,10 @@ class _ObjectScreenState extends State<ObjectScreen> {
|
||||
),
|
||||
body: Container(
|
||||
margin: const EdgeInsets.all(10.0),
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
getObjectWidget(),
|
||||
Html(data: description)
|
||||
child: Column(
|
||||
children: [
|
||||
_isLoading ? LinearProgressIndicator() : SizedBox(height: 0),
|
||||
Expanded(child: getObjectWidget()),
|
||||
]
|
||||
)
|
||||
),
|
||||
@ -178,3 +236,4 @@ class ObjectScreen extends StatefulWidget {
|
||||
@override
|
||||
_ObjectScreenState createState() => _ObjectScreenState(_object, _project, _onUpdate, _onDelete);
|
||||
}
|
||||
|
||||
|
76
mobile/lib/patterns/drawdown.dart
Normal file
76
mobile/lib/patterns/drawdown.dart
Normal file
@ -0,0 +1,76 @@
|
||||
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;
|
||||
}
|
||||
}
|
102
mobile/lib/patterns/pattern.dart
Normal file
102
mobile/lib/patterns/pattern.dart
Normal file
@ -0,0 +1,102 @@
|
||||
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),
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
41
mobile/lib/patterns/tieup.dart
Normal file
41
mobile/lib/patterns/tieup.dart
Normal file
@ -0,0 +1,41 @@
|
||||
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;
|
||||
}
|
||||
}
|
68
mobile/lib/patterns/viewer.dart
Normal file
68
mobile/lib/patterns/viewer.dart
Normal file
@ -0,0 +1,68 @@
|
||||
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'),
|
||||
]
|
||||
);*/
|
||||
}
|
||||
}
|
66
mobile/lib/patterns/warp.dart
Normal file
66
mobile/lib/patterns/warp.dart
Normal file
@ -0,0 +1,66 @@
|
||||
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;
|
||||
}
|
||||
}
|
62
mobile/lib/patterns/weft.dart
Normal file
62
mobile/lib/patterns/weft.dart
Normal file
@ -0,0 +1,62 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -2,8 +2,12 @@ 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> {
|
||||
@ -11,6 +15,7 @@ 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;
|
||||
@ -26,7 +31,6 @@ 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(() {
|
||||
@ -36,6 +40,10 @@ 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']);
|
||||
@ -65,12 +73,74 @@ 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 imageFile = await picker.getImage(source: ImageSource.gallery);
|
||||
final XFile? imageFile = await picker.pickImage(source: ImageSource.gallery);
|
||||
if (imageFile == null) return;
|
||||
file = File(imageFile.path);
|
||||
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);
|
||||
}
|
||||
on Exception {
|
||||
showDialog(
|
||||
@ -87,41 +157,6 @@ 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,27 +165,14 @@ 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.fitWidth,
|
||||
fit: BoxFit.cover,
|
||||
alignment: FractionalOffset.topCenter,
|
||||
image: new NetworkImage(url),
|
||||
)
|
||||
@ -161,7 +183,13 @@ class _ProjectScreenState extends State<ProjectScreen> {
|
||||
Widget getIconBox(Icon icon) {
|
||||
return new AspectRatio(
|
||||
aspectRatio: 1 / 1,
|
||||
child: icon
|
||||
child: new Container(
|
||||
decoration: new BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
child: icon
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -215,17 +243,12 @@ class _ProjectScreenState extends State<ProjectScreen> {
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
new ListTile(
|
||||
leading: leader,
|
||||
trailing: Icon(Icons.keyboard_arrow_right),
|
||||
title: Text(object['name']),
|
||||
subtitle: Text(type),
|
||||
),
|
||||
]
|
||||
)
|
||||
child: ListTile(
|
||||
leading: leader,
|
||||
trailing: Icon(Icons.keyboard_arrow_right),
|
||||
title: Text(object['name']),
|
||||
subtitle: Text(type),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -236,6 +259,12 @@ 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: () {
|
||||
@ -266,13 +295,50 @@ 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 something to this project using the button below.', textAlign: TextAlign.center),
|
||||
Text('Add a pattern file, an image, or something else to this project using the + button below.', textAlign: TextAlign.center),
|
||||
])
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _chooseImage,
|
||||
child: Icon(Icons.cloud_upload),
|
||||
backgroundColor: Colors.pink[500],
|
||||
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,
|
||||
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,
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -316,7 +382,6 @@ 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);
|
||||
|
@ -88,19 +88,24 @@ class _ProjectsTabState extends State<ProjectsTab> {
|
||||
),
|
||||
);
|
||||
},
|
||||
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),
|
||||
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)
|
||||
),
|
||||
]
|
||||
),
|
||||
trailing: Icon(Icons.keyboard_arrow_right),
|
||||
title: Text(project['name'] != null ? project['name'] : 'Untitled project'),
|
||||
subtitle: Text(description),
|
||||
),
|
||||
)
|
||||
))
|
||||
)
|
||||
;
|
||||
}
|
||||
|
@ -41,6 +41,14 @@ 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(
|
||||
@ -50,6 +58,7 @@ class _UserScreenState extends State<UserScreen> {
|
||||
)
|
||||
: Container(
|
||||
padding: EdgeInsets.all(10),
|
||||
margin: EdgeInsets.only(top: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
@ -60,11 +69,12 @@ class _UserScreenState extends State<UserScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(_user['username'], style: Theme.of(context).textTheme.titleMedium),
|
||||
Text(_user['username'], style: Theme.of(context).textTheme.titleLarge),
|
||||
SizedBox(height: 5),
|
||||
_user['location'] != null ?
|
||||
Row(children: [
|
||||
Icon(CupertinoIcons.location),
|
||||
SizedBox(width: 10),
|
||||
Text(_user['location'])
|
||||
]) : SizedBox(height: 1),
|
||||
SizedBox(height: 10),
|
||||
|
@ -1,31 +1,83 @@
|
||||
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 a = AssetImage('assets/avatars/9.png');
|
||||
ImageProvider? avatarUrl(Map<String,dynamic> user) {
|
||||
if (user != null && user['avatar'] != null) {
|
||||
if (user['avatar'].length < 3) {
|
||||
a = AssetImage('assets/avatars/${user['avatar']}.png');
|
||||
return AssetImage('assets/avatars/${user['avatar']}.png');
|
||||
}
|
||||
else {
|
||||
a =NetworkImage(user['avatarUrl']);
|
||||
return NetworkImage(user['avatarUrl']);
|
||||
}
|
||||
}
|
||||
return a;
|
||||
return null;
|
||||
}
|
||||
|
||||
Widget avatarImage(ImageProvider image, {double size=30}) {
|
||||
Widget avatarImage(ImageProvider? image, {double size=30}) {
|
||||
if (image != null) {
|
||||
return new Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: new BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image: new DecorationImage(
|
||||
fit: BoxFit.fill,
|
||||
image: image
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
return new Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: new BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image: new DecorationImage(
|
||||
fit: BoxFit.fill,
|
||||
image: image
|
||||
)
|
||||
)
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
@ -29,10 +29,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0
|
||||
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.10.0"
|
||||
version: "2.11.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -45,10 +45,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c
|
||||
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
version: "1.3.0"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -61,10 +61,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0
|
||||
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
version: "1.17.2"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -129,6 +129,14 @@ 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:
|
||||
@ -214,6 +222,14 @@ 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:
|
||||
@ -284,50 +300,50 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker
|
||||
sha256: "6432178560d95303cc70d038363f892f5a05750dd27bc55220c7301af54d05e9"
|
||||
sha256: "340efe08645537d6b088a30620ee5752298b1630f23a829181172610b868262b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.8"
|
||||
version: "1.0.6"
|
||||
image_picker_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: "1ec6830289f5b6aeff3aa8239ea737c71950178dda389342dc2215adb06b4bd8"
|
||||
sha256: "1a27bf4cc0330389cebe465bab08fe6dec97e44015b4899637344bb7297759ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.6+20"
|
||||
version: "0.8.9+2"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_for_web
|
||||
sha256: "98f50d6b9f294c8ba35e25cc0d13b04bfddd25dbc8d32fa9d566a6572f2c081c"
|
||||
sha256: "869fe8a64771b7afbc99fc433a5f7be2fea4d1cb3d7c11a48b6b579eb9c797f0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.12"
|
||||
version: "2.2.0"
|
||||
image_picker_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_ios
|
||||
sha256: d779210bda268a03b57e923fb1e410f32f5c5e708ad256348bcbf1f44f558fd0
|
||||
sha256: eac0a62104fa12feed213596df0321f57ce5a572562f72a68c4ff81e9e4caacf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.7+4"
|
||||
version: "0.8.9"
|
||||
image_picker_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_linux
|
||||
sha256: "1d8f9a97178d6b8a035f1d2765f17f8ca3d36a40d5594e742a481b1e002f20be"
|
||||
sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
version: "0.2.1+1"
|
||||
image_picker_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_macos
|
||||
sha256: ff094b36d6c06200808f733144a033e45b4e17d59524e1cf7d2af7e4cb94e1ab
|
||||
sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
version: "0.2.1+1"
|
||||
image_picker_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -340,10 +356,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_windows
|
||||
sha256: bf77b819eb62c487e6af53b9eb213adc12bd060ef7e43f3b1dd69c53cc24a61d
|
||||
sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
version: "0.2.1+1"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -372,26 +388,34 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72"
|
||||
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.13"
|
||||
version: "0.12.16"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
|
||||
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
version: "0.5.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42"
|
||||
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
version: "1.9.1"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -404,34 +428,58 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b
|
||||
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.2"
|
||||
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"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.11"
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec"
|
||||
sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.6"
|
||||
version: "2.1.1"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96"
|
||||
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.7"
|
||||
version: "2.2.1"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -480,6 +528,22 @@ 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:
|
||||
@ -545,10 +609,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
|
||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
version: "1.10.0"
|
||||
sprintf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sprintf
|
||||
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -585,10 +657,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206
|
||||
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.16"
|
||||
version: "0.6.0"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -661,6 +733,14 @@ 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:
|
||||
@ -669,14 +749,22 @@ 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: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c"
|
||||
sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.4"
|
||||
version: "5.0.6"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -702,5 +790,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=2.19.0 <3.0.0"
|
||||
flutter: ">=3.3.0"
|
||||
dart: ">=3.1.0-185.0.dev <4.0.0"
|
||||
flutter: ">=3.10.0"
|
||||
|
@ -30,9 +30,14 @@ dependencies:
|
||||
url_launcher: ^6.1.2
|
||||
flutter_html: ^3.0.0-alpha.3
|
||||
intl: ^0.17.0
|
||||
image_picker: ^0.8.5+3
|
||||
image_picker: ^1.0.6
|
||||
file_picker: ^6.1.1
|
||||
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:
|
||||
|
@ -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}}
|
||||
<Dropdown direction="left" pointing="top right" icon={null} style={{ marginTop: 10, zIndex: 30}}
|
||||
trigger={<UserChip user={user} withoutLink avatarOnly />}
|
||||
>
|
||||
<Dropdown.Menu style={{ minWidth: '200px', paddingTop: 10 }}>
|
||||
|
@ -111,7 +111,7 @@ function ObjectViewer() {
|
||||
}
|
||||
{(utils.canEditProject(user, project) || project.openSource) &&
|
||||
<React.Fragment>
|
||||
{object.patternImage &&
|
||||
{(object.fullPreviewUrl || object.patternImage) &&
|
||||
<Dropdown.Item icon='file outline' content='Download complete pattern as an image' onClick={e => utils.downloadPatternImage(object)}/>
|
||||
}
|
||||
<Dropdown.Divider />
|
||||
|
@ -43,7 +43,6 @@ function Draft() {
|
||||
dispatch(actions.objects.receive(o));
|
||||
setObject(o);
|
||||
setPattern(o.pattern);
|
||||
setTimeout(() => generatePreviews(o), 1000);
|
||||
});
|
||||
}, [objectId]);
|
||||
|
||||
@ -63,20 +62,10 @@ 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));
|
||||
|
@ -34,10 +34,12 @@ function DraftPreview({ object }) {
|
||||
dispatch(actions.objects.update(objectId, 'previewUrl', previewUrl));
|
||||
});
|
||||
}
|
||||
// Generate the entire pattern and store in memory
|
||||
util.generateCompletePattern(o.pattern, `.preview-${objectId}`, image => {
|
||||
dispatch(actions.objects.update(objectId, 'patternImage', image));
|
||||
});
|
||||
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));
|
||||
|
@ -135,7 +135,7 @@ const utils = {
|
||||
downloadPatternImage(object) {
|
||||
const element = document.createElement('a');
|
||||
element.setAttribute('target', '_blank');
|
||||
element.setAttribute('href', object.patternImage);
|
||||
element.setAttribute('href', object.fullPreviewUrl || object.patternImage);
|
||||
element.setAttribute('download', `${object.name.replace(/ /g, '_')}-pattern.png`);
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
|
Loading…
Reference in New Issue
Block a user