Compare commits

..

32 Commits

Author SHA1 Message Date
6e15952ffc fix bug with dropdown nav being overlapped
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-01-10 19:05:00 +00:00
20b94e553d re-use preview URLs to save on disk space 2024-01-10 18:57:47 +00:00
572d39e947 mobile improvements 2024-01-07 23:10:20 +00:00
dcf44f6b1d improved control over when pattern previews are generated 2024-01-07 22:48:32 +00:00
4b410ec31e improvements for user avatar handling 2024-01-07 22:29:09 +00:00
f63866a04c mobile improvements 2024-01-07 21:52:02 +00:00
104879ee27 mobile improvements 2024-01-07 18:12:35 +00:00
f94769e228 mobile improvements 2024-01-07 17:51:22 +00:00
88c0b44444 add support for interlacement view in previews generate 2024-01-07 17:42:14 +00:00
3403134072 generate images on creation/update 2024-01-07 14:17:10 +00:00
e6178c8a72 code to upload images to s3 2024-01-07 12:21:39 +00:00
58bf8ca74e support for drawdown only 2024-01-07 11:59:32 +00:00
6cfcf0c5a1 complete drawdown image creation 2024-01-07 11:51:11 +00:00
65b379f162 serverside colourways 2024-01-07 11:29:58 +00:00
4bf03c7c67 serverside weft drawing 2024-01-07 10:56:59 +00:00
aeb60dd840 serverside warp drawing 2024-01-07 10:47:30 +00:00
3b2c1e7f4c allow for import of multiple types of files 2024-01-06 16:44:55 +00:00
a2cde7de81 add basic file chooser 2024-01-06 13:01:12 +00:00
b14f438597 add ability to share projects 2024-01-06 12:39:33 +00:00
ac97481e6e add ability to share objects 2024-01-06 12:22:25 +00:00
1129a9df48 improvements to pattern viewer 2024-01-06 11:03:29 +00:00
afc32578cf introduce interlacement view 2024-01-05 18:49:39 +00:00
f44d56182b default positioning for canvas on-load 2024-01-05 18:33:15 +00:00
14f930af13 use a InteractiveViewer for panning a pattern 2024-01-05 17:57:10 +00:00
9ed84493cc fixed drawdown colour bug 2024-01-04 22:27:30 +00:00
ee11984a00 implemented drawdown 2024-01-04 22:09:56 +00:00
dcb9453ccd drawdown grid 2024-01-04 20:57:38 +00:00
45725f52c1 weft colourway 2024-01-03 23:49:34 +00:00
5e108cf8c8 warp colourway 2024-01-03 23:10:23 +00:00
1406f9c4c8 warp weft tieup rendering 2024-01-03 22:35:09 +00:00
3e79d950b1 grid drawing working 2024-01-03 21:55:44 +00:00
ea792bd75d some initial canvas work 2024-01-03 20:36:28 +00:00
29 changed files with 2063 additions and 1010 deletions

View File

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

View File

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

View File

@ -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): def get_file(key):
s3 = get_s3() s3 = get_s3()
return s3.get_object( return s3.get_object(

1663
api/poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,4 +1,7 @@
import io, time
import configparser import configparser
from PIL import Image, ImageDraw
from api import uploads
def normalise_colour(max_color, triplet): def normalise_colour(max_color, triplet):
color_factor = 256/max_color 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)))) new_components.append(str(int(float(color_factor) * int(component))))
return ','.join(new_components) 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): def get_colour_index(colours, colour):
for (index, c) in enumerate(colours): for (index, c) in enumerate(colours):
if c == colour: return index + 1 if c == colour: return index + 1
@ -197,3 +213,189 @@ def loads(wif_file):
draft['tieups'][int(x)-1] = [] draft['tieups'][int(x)-1] = []
return draft 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,4 +1,38 @@
PODS: 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): - Firebase/CoreOnly (10.9.0):
- FirebaseCore (= 10.9.0) - FirebaseCore (= 10.9.0)
- Firebase/Messaging (10.9.0): - Firebase/Messaging (10.9.0):
@ -60,23 +94,37 @@ PODS:
- nanopb/encode (= 2.30909.0) - nanopb/encode (= 2.30909.0)
- nanopb/decode (2.30909.0) - nanopb/decode (2.30909.0)
- nanopb/encode (2.30909.0) - nanopb/encode (2.30909.0)
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- PromisesObjC (2.2.0) - 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): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- SwiftyGif (5.4.4)
- url_launcher_ios (0.0.1): - url_launcher_ios (0.0.1):
- Flutter - Flutter
DEPENDENCIES: DEPENDENCIES:
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - 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`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- DKImagePickerController
- DKPhotoGallery
- Firebase - Firebase
- FirebaseCore - FirebaseCore
- FirebaseCoreInternal - FirebaseCoreInternal
@ -86,8 +134,12 @@ SPEC REPOS:
- GoogleUtilities - GoogleUtilities
- nanopb - nanopb
- PromisesObjC - PromisesObjC
- SDWebImage
- SwiftyGif
EXTERNAL SOURCES: EXTERNAL SOURCES:
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
firebase_core: firebase_core:
:path: ".symlinks/plugins/firebase_core/ios" :path: ".symlinks/plugins/firebase_core/ios"
firebase_messaging: firebase_messaging:
@ -96,12 +148,19 @@ EXTERNAL SOURCES:
:path: Flutter :path: Flutter
image_picker_ios: image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/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: shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/ios" :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
url_launcher_ios: url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios" :path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de
Firebase: bd152f0f3d278c4060c5c71359db08ebcfd5a3e2 Firebase: bd152f0f3d278c4060c5c71359db08ebcfd5a3e2
firebase_core: ce64b0941c6d87c6ef5022ae9116a158236c8c94 firebase_core: ce64b0941c6d87c6ef5022ae9116a158236c8c94
firebase_messaging: 42912365e62efc1ea3e00724e5eecba6068ddb88 firebase_messaging: 42912365e62efc1ea3e00724e5eecba6068ddb88
@ -114,10 +173,14 @@ SPEC CHECKSUMS:
GoogleUtilities: 9aa0ad5a7bc171f8bae016300bfcfa3fb8425749 GoogleUtilities: 9aa0ad5a7bc171f8bae016300bfcfa3fb8425749
image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef
SDWebImage: a81bbb3ba4ea5f810f4069c68727cb118467a04a
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
COCOAPODS: 1.12.0 COCOAPODS: 1.14.2

View File

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

View File

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

View File

@ -3,12 +3,13 @@ import 'package:http/http.dart' as http;
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'util.dart';
class Api { class Api {
String? _token; String? _token;
final String apiBase = 'https://api.treadl.com'; //final String apiBase = 'https://api.treadl.com';
//final String apiBase = 'http://192.168.5.86:2001'; final String apiBase = 'http://192.168.5.134:2001';
Future<String?> loadToken() async { Future<String?> loadToken() async {
if (_token != null) { if (_token != null) {
@ -102,4 +103,18 @@ class Api {
int status = response.statusCode; int status = response.statusCode;
return status == 200; 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) { Widget buildGroupCard(Map<String,dynamic> group) {
String description = group['description']; String? description = group['description'];
if (description != null && description.length > 80) { if (description != null && description.length > 80) {
description = description.substring(0, 77) + '...'; description = description.substring(0, 77) + '...';
} else { } else {
@ -44,16 +44,11 @@ class _GroupsTabState extends State<GroupsTab> {
), ),
); );
}, },
child: Column( child: ListTile(
mainAxisSize: MainAxisSize.min, leading: Icon(Icons.people),
children: <Widget>[ trailing: Icon(Icons.keyboard_arrow_right),
new ListTile( title: Text(group['name']),
leading: Icon(Icons.people), subtitle: Text(description.replaceAll("\n", " ")),
trailing: Icon(Icons.keyboard_arrow_right),
title: Text(group['name']),
subtitle: Text(description.replaceAll("\n", " ")),
),
]
) )
) )
) )

View File

@ -3,17 +3,60 @@ import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/flutter_html.dart';
import 'dart:io';
import 'api.dart'; import 'api.dart';
import 'util.dart';
import 'patterns/pattern.dart';
import 'patterns/viewer.dart';
class _ObjectScreenState extends State<ObjectScreen> { class _ObjectScreenState extends State<ObjectScreen> {
final Map<String,dynamic> _project; final Map<String,dynamic> _project;
Map<String,dynamic> _object; Map<String,dynamic> _object;
Map<String,dynamic>? _pattern;
bool _isLoading = false;
final Function _onUpdate; final Function _onUpdate;
final Function _onDelete; final Function _onDelete;
final Api api = Api(); final Api api = Api();
final Util util = Util();
_ObjectScreenState(this._object, this._project, this._onUpdate, this._onDelete) { } _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 { void _deleteObject(BuildContext context, BuildContext modalContext) async {
var data = await api.request('DELETE', '/objects/' + _object['_id']); var data = await api.request('DELETE', '/objects/' + _object['_id']);
if (data['success']) { if (data['success']) {
@ -68,7 +111,6 @@ class _ObjectScreenState extends State<ObjectScreen> {
TextButton( TextButton(
child: Text('OK'), child: Text('OK'),
onPressed: () async { onPressed: () async {
print(renameController.text);
var data = await api.request('PUT', '/objects/' + _object['_id'], {'name': renameController.text}); var data = await api.request('PUT', '/objects/' + _object['_id'], {'name': renameController.text});
if (data['success']) { if (data['success']) {
Navigator.pop(context); Navigator.pop(context);
@ -118,7 +160,10 @@ class _ObjectScreenState extends State<ObjectScreen> {
return Image.network(_object['url']); return Image.network(_object['url']);
} }
else if (_object['type'] == 'pattern') { 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']!);; return Image.network(_object['previewUrl']!);;
} }
else { else {
@ -133,9 +178,16 @@ class _ObjectScreenState extends State<ObjectScreen> {
} }
} }
else { else {
return ElevatedButton(child: Text('View file'), onPressed: () { return Center(child: Column(
launch(_object['url']); 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( appBar: AppBar(
title: Text(_object['name']), title: Text(_object['name']),
actions: <Widget>[ actions: <Widget>[
IconButton(
icon: Icon(Icons.ios_share),
onPressed: () {
_shareObject();
},
),
IconButton( IconButton(
icon: Icon(Icons.settings), icon: Icon(Icons.settings),
onPressed: () { onPressed: () {
@ -158,10 +216,10 @@ class _ObjectScreenState extends State<ObjectScreen> {
), ),
body: Container( body: Container(
margin: const EdgeInsets.all(10.0), margin: const EdgeInsets.all(10.0),
child: ListView( child: Column(
children: <Widget>[ children: [
getObjectWidget(), _isLoading ? LinearProgressIndicator() : SizedBox(height: 0),
Html(data: description) Expanded(child: getObjectWidget()),
] ]
) )
), ),
@ -178,3 +236,4 @@ class ObjectScreen extends StatefulWidget {
@override @override
_ObjectScreenState createState() => _ObjectScreenState(_object, _project, _onUpdate, _onDelete); _ObjectScreenState createState() => _ObjectScreenState(_object, _project, _onUpdate, _onDelete);
} }

View 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;
}
}

View 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),
),
)
]
)
);
}
}

View 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;
}
}

View 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'),
]
);*/
}
}

View 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;
}
}

View 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;
}
}

View File

@ -2,8 +2,12 @@ import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:image_picker/image_picker.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 'dart:io';
import 'api.dart'; import 'api.dart';
import 'util.dart';
import 'object.dart'; import 'object.dart';
class _ProjectScreenState extends State<ProjectScreen> { class _ProjectScreenState extends State<ProjectScreen> {
@ -11,6 +15,7 @@ class _ProjectScreenState extends State<ProjectScreen> {
final Function _onDelete; final Function _onDelete;
final picker = ImagePicker(); final picker = ImagePicker();
final Api api = Api(); final Api api = Api();
final Util util = Util();
Map<String,dynamic> _project; Map<String,dynamic> _project;
List<dynamic> _objects = []; List<dynamic> _objects = [];
bool _loading = false; bool _loading = false;
@ -26,7 +31,6 @@ class _ProjectScreenState extends State<ProjectScreen> {
void getObjects(String fullName) async { void getObjects(String fullName) async {
setState(() => _loading = true); setState(() => _loading = true);
print(fullName);
var data = await api.request('GET', '/projects/' + fullName + '/objects'); var data = await api.request('GET', '/projects/' + fullName + '/objects');
if (data['success'] == true) { if (data['success'] == true) {
setState(() { setState(() {
@ -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() { void _onDeleteProject() {
Navigator.pop(context); Navigator.pop(context);
_onDelete(_project['_id']); _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 { void _chooseImage() async {
File file; File file;
try { try {
final imageFile = await picker.getImage(source: ImageSource.gallery); final XFile? imageFile = await picker.pickImage(source: ImageSource.gallery);
if (imageFile == null) return; 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 { on Exception {
showDialog( 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); 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) { Widget getNetworkImageBox(String url) {
return new AspectRatio( return new AspectRatio(
aspectRatio: 1 / 1, aspectRatio: 1 / 1,
child: new Container( child: new Container(
decoration: new BoxDecoration( decoration: new BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
image: new DecorationImage( image: new DecorationImage(
fit: BoxFit.fitWidth, fit: BoxFit.cover,
alignment: FractionalOffset.topCenter, alignment: FractionalOffset.topCenter,
image: new NetworkImage(url), image: new NetworkImage(url),
) )
@ -161,7 +183,13 @@ class _ProjectScreenState extends State<ProjectScreen> {
Widget getIconBox(Icon icon) { Widget getIconBox(Icon icon) {
return new AspectRatio( return new AspectRatio(
aspectRatio: 1 / 1, 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( child: ListTile(
mainAxisSize: MainAxisSize.min, leading: leader,
children: <Widget>[ trailing: Icon(Icons.keyboard_arrow_right),
new ListTile( title: Text(object['name']),
leading: leader, subtitle: Text(type),
trailing: Icon(Icons.keyboard_arrow_right), ),
title: Text(object['name']),
subtitle: Text(type),
),
]
)
) )
); );
} }
@ -236,6 +259,12 @@ class _ProjectScreenState extends State<ProjectScreen> {
appBar: AppBar( appBar: AppBar(
title: Text(_project['name']), title: Text(_project['name']),
actions: <Widget>[ actions: <Widget>[
IconButton(
icon: Icon(Icons.ios_share),
onPressed: () {
_shareProject();
},
),
IconButton( IconButton(
icon: Icon(Icons.settings), icon: Icon(Icons.settings),
onPressed: () { onPressed: () {
@ -266,13 +295,50 @@ class _ProjectScreenState extends State<ProjectScreen> {
children: [ children: [
Text('This project is currently empty', style: TextStyle(fontSize: 20), textAlign: TextAlign.center), Text('This project is currently empty', style: TextStyle(fontSize: 20), textAlign: TextAlign.center),
Image(image: AssetImage('assets/empty.png'), width: 300), 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( floatingActionButtonLocation: ExpandableFab.location,
onPressed: _chooseImage, floatingActionButton: ExpandableFab(
child: Icon(Icons.cloud_upload), distance: 70,
backgroundColor: Colors.pink[500], 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( TextButton(
child: Text('OK'), child: Text('OK'),
onPressed: () async { onPressed: () async {
print(renameController.text);
var data = await api.request('PUT', '/projects/' + _project['owner']['username'] + '/' + _project['path'], {'name': renameController.text}); var data = await api.request('PUT', '/projects/' + _project['owner']['username'] + '/' + _project['path'], {'name': renameController.text});
if (data['success']) { if (data['success']) {
Navigator.pop(context); Navigator.pop(context);

View File

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

View File

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

View File

@ -1,31 +1,83 @@
import 'package:flutter/material.dart'; 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 { 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 != null && user['avatar'] != null) {
if (user['avatar'].length < 3) { if (user['avatar'].length < 3) {
a = AssetImage('assets/avatars/${user['avatar']}.png'); return AssetImage('assets/avatars/${user['avatar']}.png');
} }
else { 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( return new Container(
width: size, width: size,
height: size, height: size,
decoration: new BoxDecoration( child: Icon(Icons.person)
shape: BoxShape.circle,
image: new DecorationImage(
fit: BoxFit.fill,
image: image
)
)
); );
} }
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 dependency: transitive
description: description:
name: async name: async
sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.10.0" version: "2.11.0"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -45,10 +45,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" version: "1.3.0"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@ -61,10 +61,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: collection name: collection
sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.17.0" version: "1.17.2"
convert: convert:
dependency: transitive dependency: transitive
description: description:
@ -129,6 +129,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.4" 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: file_selector_linux:
dependency: transitive dependency: transitive
description: description:
@ -214,6 +222,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: flutter_html:
dependency: "direct main" dependency: "direct main"
description: description:
@ -284,50 +300,50 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: image_picker name: image_picker
sha256: "6432178560d95303cc70d038363f892f5a05750dd27bc55220c7301af54d05e9" sha256: "340efe08645537d6b088a30620ee5752298b1630f23a829181172610b868262b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.8.8" version: "1.0.6"
image_picker_android: image_picker_android:
dependency: transitive dependency: transitive
description: description:
name: image_picker_android name: image_picker_android
sha256: "1ec6830289f5b6aeff3aa8239ea737c71950178dda389342dc2215adb06b4bd8" sha256: "1a27bf4cc0330389cebe465bab08fe6dec97e44015b4899637344bb7297759ec"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.8.6+20" version: "0.8.9+2"
image_picker_for_web: image_picker_for_web:
dependency: transitive dependency: transitive
description: description:
name: image_picker_for_web name: image_picker_for_web
sha256: "98f50d6b9f294c8ba35e25cc0d13b04bfddd25dbc8d32fa9d566a6572f2c081c" sha256: "869fe8a64771b7afbc99fc433a5f7be2fea4d1cb3d7c11a48b6b579eb9c797f0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.12" version: "2.2.0"
image_picker_ios: image_picker_ios:
dependency: transitive dependency: transitive
description: description:
name: image_picker_ios name: image_picker_ios
sha256: d779210bda268a03b57e923fb1e410f32f5c5e708ad256348bcbf1f44f558fd0 sha256: eac0a62104fa12feed213596df0321f57ce5a572562f72a68c4ff81e9e4caacf
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.8.7+4" version: "0.8.9"
image_picker_linux: image_picker_linux:
dependency: transitive dependency: transitive
description: description:
name: image_picker_linux name: image_picker_linux
sha256: "1d8f9a97178d6b8a035f1d2765f17f8ca3d36a40d5594e742a481b1e002f20be" sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.0" version: "0.2.1+1"
image_picker_macos: image_picker_macos:
dependency: transitive dependency: transitive
description: description:
name: image_picker_macos name: image_picker_macos
sha256: ff094b36d6c06200808f733144a033e45b4e17d59524e1cf7d2af7e4cb94e1ab sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.0" version: "0.2.1+1"
image_picker_platform_interface: image_picker_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -340,10 +356,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image_picker_windows name: image_picker_windows
sha256: bf77b819eb62c487e6af53b9eb213adc12bd060ef7e43f3b1dd69c53cc24a61d sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.0" version: "0.2.1+1"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@ -372,26 +388,34 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.13" version: "0.12.16"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.0" version: "0.5.0"
meta: meta:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted 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: nested:
dependency: transitive dependency: transitive
description: description:
@ -404,34 +428,58 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path name: path
sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted 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: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
name: path_provider_linux name: path_provider_linux
sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57 sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.11" version: "2.2.1"
path_provider_platform_interface: path_provider_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: path_provider_platform_interface name: path_provider_platform_interface
sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.6" version: "2.1.1"
path_provider_windows: path_provider_windows:
dependency: transitive dependency: transitive
description: description:
name: path_provider_windows name: path_provider_windows
sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.7" version: "2.2.1"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
@ -480,6 +528,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.5" 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: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:
@ -545,10 +609,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: source_span name: source_span
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted 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: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -585,10 +657,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.4.16" version: "0.6.0"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@ -661,6 +733,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.6" version: "3.0.6"
uuid:
dependency: transitive
description:
name: uuid
sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f"
url: "https://pub.dev"
source: hosted
version: "4.2.2"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@ -669,14 +749,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.1.4"
web:
dependency: transitive
description:
name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
url: "https://pub.dev"
source: hosted
version: "0.1.4-beta"
win32: win32:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.4" version: "5.0.6"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@ -702,5 +790,5 @@ packages:
source: hosted source: hosted
version: "3.1.2" version: "3.1.2"
sdks: sdks:
dart: ">=2.19.0 <3.0.0" dart: ">=3.1.0-185.0.dev <4.0.0"
flutter: ">=3.3.0" flutter: ">=3.10.0"

View File

@ -30,9 +30,14 @@ dependencies:
url_launcher: ^6.1.2 url_launcher: ^6.1.2
flutter_html: ^3.0.0-alpha.3 flutter_html: ^3.0.0-alpha.3
intl: ^0.17.0 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 flutter_launcher_icons: ^0.9.0
firebase_messaging: ^14.4.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 #fluttertoast: ^8.0.9
dev_dependencies: dev_dependencies:

View File

@ -114,7 +114,7 @@ export default function NavBar() {
<Menu.Menu position='right'> <Menu.Menu position='right'>
{isAuthenticated && <> {isAuthenticated && <>
<Menu.Item className='abovee-mobile'><SearchBar /></Menu.Item> <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 />} trigger={<UserChip user={user} withoutLink avatarOnly />}
> >
<Dropdown.Menu style={{ minWidth: '200px', paddingTop: 10 }}> <Dropdown.Menu style={{ minWidth: '200px', paddingTop: 10 }}>

View File

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

View File

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

View File

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

View File

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