Compare commits

..

No commits in common. "2f92b3b88330252135824e13e172663992d6436d" and "3b691fed816f51b1aada8d17516ef9e0cc99761e" have entirely different histories.

31 changed files with 450 additions and 597 deletions

View File

@ -53,7 +53,6 @@ def copy_to_project(user, id, project_id):
obj['project'] = target_project['_id']
obj['createdAt'] = datetime.datetime.now()
obj['commentCount'] = 0
if 'preview' in obj: del obj['preview']
db.objects.insert_one(obj)
return obj

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -23,7 +23,7 @@
"react-joyride": "^2.4.0",
"react-redux": "^8.0.1",
"react-rewards": "^2.0.3",
"react-router-dom": "^6.21.1",
"react-router-dom": "^6.3.0",
"react-toastify": "^4.4.0",
"redux": "^4.0.0",
"sanitize-html": "^1.19.1",

View File

@ -3,6 +3,8 @@ import { store } from '../index';
export default {
INIT_DRIFT: 'INIT_DRIFT',
SYNC_DRIFT: 'SYNC_DRIFT',
REQUEST_USERS: 'REQUEST_USERS',
REQUEST_FAILED: 'REQUEST_FAILED',
RECEIVE_USER: 'RECEIVE_USERS',
@ -13,6 +15,14 @@ export default {
LEAVE_GROUP: 'LEAVE_GROUP',
UPDATE_SUBSCRIPTIONS: 'UPDATE_SUBSCRIPTIONS',
initDrift() {
return { type: this.INIT_DRIFT };
},
syncDrift(synced) {
return { type: this.SYNC_DRIFT, synced };
},
request() {
return { type: this.REQUEST_USERS };
},

View File

@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import { Outlet } from 'react-router-dom';
import { Routes, Route } from 'react-router-dom';
import { Helmet } from 'react-helmet';
import { useDispatch, useSelector } from 'react-redux';
import { ToastContainer, toast } from 'react-toastify';
@ -11,7 +11,47 @@ import actions from '../actions';
import utils from '../utils/utils.js';
import NavBar from './includes/NavBar';
import Footer from './includes/Footer';
import MarketingHome from './marketing/Home';
import PrivacyPolicy from './marketing/PrivacyPolicy';
import TermsOfUse from './marketing/TermsOfUse';
import Login from './Login';
import ForgottenPassword from './ForgottenPassword';
import ResetPassword from './ResetPassword';
import Home from './main/Home';
import Profile from './main/users/Profile';
import ProfileEdit from './main/users/EditProfile';
import ProfileProjects from './main/users/ProfileProjects';
import NewProject from './main/projects/New';
import Project from './main/projects/Project';
import ProjectObjects from './main/projects/ProjectObjects';
import ProjectSettings from './main/projects/Settings';
import ObjectDraft from './main/projects/objects/Draft';
import ObjectList from './main/projects/ObjectList';
import Settings from './main/settings/Settings';
import SettingsIdentity from './main/settings/Identity';
import SettingsNotification from './main/settings/Notification';
import SettingsAccount from './main/settings/Account';
import NewGroup from './main/groups/New';
import Group from './main/groups/Group';
import GroupFeed from './main/groups/Feed';
import GroupMembers from './main/groups/Members';
import GroupProjects from './main/groups/Projects';
import GroupSettings from './main/groups/Settings';
import Explore from './main/explore/Explore'
import Docs from './docs';
import DocsHome from './docs/Home';
import DocsDoc from './docs/Doc';
import Root from './main/root';
const StyledContent = styled.div`
display: flex;
@ -27,10 +67,11 @@ const GlobalStyle = createGlobalStyle`
function App() {
const dispatch = useDispatch();
const { isAuthenticated, isAuthenticating, isAuthenticatingType, user } = useSelector(state => {
const { isAuthenticated, isAuthenticating, isAuthenticatingType, user, driftReady, syncedToDrift } = useSelector(state => {
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const { isAuthenticated, isAuthenticating, isAuthenticatingType } = state.auth;
return { isAuthenticated, isAuthenticating, isAuthenticatingType, user };
const { driftReady, syncedToDrift } = state.users;
return { isAuthenticated, isAuthenticating, isAuthenticatingType, user, driftReady, syncedToDrift };
});
const loggedInUserId = user?._id;
@ -53,17 +94,74 @@ function App() {
});
}, [dispatch, loggedInUserId]);
useEffect(() => {
window.drift && window.drift.on('ready', () => {
dispatch(actions.users.initDrift());
});
}, [dispatch]);
useEffect(() => {
if (user && driftReady && !syncedToDrift && window.drift) {
window.drift.identify(user._id, {
email: user.email,
username: user.username,
createdAt: user.createdAt,
});
dispatch(actions.users.syncDrift(null));
}
}, [dispatch, user, driftReady, syncedToDrift]);
return (
<StyledContent>
<GlobalStyle whiteColor />
<Helmet defaultTitle={utils.appName()} titleTemplate={`%s | ${utils.appName()}`} />
<NavBar />
<div style={{ flex: '1' }}>
<Outlet />
<Routes>
<Route end path="/" element={isAuthenticated
? <Home />
: <MarketingHome onRegisterClicked={() => dispatch(actions.auth.openRegister())} />
} />
<Route path="/privacy" element={<PrivacyPolicy />} />
<Route path="/terms-of-use" element={<TermsOfUse />} />
<Route path="/password/forgotten" element={<ForgottenPassword />} />
<Route path="/password/reset" element={<ResetPassword />} />
<Route path="/settings" element={<Settings />}>
<Route path='identity' element={<SettingsIdentity />} />
<Route path='notifications' element={<SettingsNotification />} />
<Route path='account' element={<SettingsAccount />} />
<Route path='' element={<SettingsIdentity />} />
</Route>
<Route path="/projects/new" element={<NewProject />} />
<Route path="/groups/new" element={<NewGroup />} />
<Route path="/groups/:id" element={<Group />}>
<Route path='feed' element={<GroupFeed />} />
<Route path='members' element={<GroupMembers />} />
<Route path='projects' element={<GroupProjects />} />
<Route path='settings' element={<GroupSettings />} />
<Route path='' end element={<GroupFeed />} />
</Route>
<Route path='/root' element={<Root />} />
<Route path='/docs' element={<Docs />}>
<Route path=":doc" element={<DocsDoc />} />
<Route path='' element={<DocsHome />} />
</Route>
<Route path='/:username/:projectPath' element={<Project />}>
<Route path="settings" element={<ProjectSettings />} />
<Route path=":objectId/edit" element={<ObjectDraft />} />
<Route path=":objectId" element={<ProjectObjects />} />
<Route path='' element={<ObjectList />} />
</Route>
<Route path='/explore' element={<Explore />} />
<Route path="/:username" element={<Profile />}>
<Route path="edit" element={<ProfileEdit />} />
<Route path='' element={<ProfileProjects />} />
</Route>
</Routes>
<Login open={isAuthenticating} authType={isAuthenticatingType} onClose={() => dispatch(actions.auth.closeAuthentication())} />
<ToastContainer position={toast.POSITION.BOTTOM_CENTER} hideProgressBar/>
<Divider hidden section />
</div>
<Login open={isAuthenticating} authType={isAuthenticatingType} onClose={() => dispatch(actions.auth.closeAuthentication())} />
<ToastContainer position={toast.POSITION.BOTTOM_CENTER} hideProgressBar/>
<Divider hidden section />
<Footer />
</StyledContent>
);

View File

@ -3,7 +3,7 @@ import {
Message, Modal, Grid, Form, Input, Button,
} from 'semantic-ui-react';
import { useDispatch, useSelector } from 'react-redux';
import { Link, useNavigate, useLocation } from 'react-router-dom';
import { Link } from 'react-router-dom';
import actions from '../actions';
import { api } from '../api';
import utils from '../utils/utils.js';
@ -16,8 +16,6 @@ function Login({ open, authType, onClose }) {
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const dispatch = useDispatch();
const navigate = useNavigate();
const location = useLocation();
const { error } = useSelector(state => {
const { loading, error } = state.auth;
@ -33,7 +31,6 @@ function Login({ open, authType, onClose }) {
setUsername('');
dispatch(actions.auth.receiveLogin(data));
onClose();
if (location.pathname === '/') navigate('/projects');
}, (err) => {
dispatch(actions.auth.loginError(err));
setLoading(false);
@ -49,7 +46,6 @@ function Login({ open, authType, onClose }) {
setUsername('');
dispatch(actions.auth.receiveLogin(data));
onClose();
if (location.pathname === '/') navigate('/projects');
}, (err) => {
dispatch(actions.auth.loginError(err));
setLoading(false);

View File

@ -1,24 +0,0 @@
import React from 'react';
import { Container, Segment, Button, Message } from 'semantic-ui-react';
import { useDispatch } from 'react-redux';
import utils from '../../utils/utils.js';
import actions from '../../actions';
export default function LoginNeeded() {
const dispatch = useDispatch();
return (
<Container style={{marginTop: 30}}>
<Message info>
<h3>Welcome to {utils.appName()}</h3>
<p>You need to have a {utils.appName()} account in order to access this. Please login or register first.</p>
<Button.Group style={{marginTop: 30}}>
<Button color="white" onClick={() => dispatch(actions.auth.openLogin())}>Login</Button>
<Button color="teal" onClick={() => dispatch(actions.auth.openRegister())}>Register</Button>
</Button.Group>
</Message>
</Container>
);
}

View File

@ -51,6 +51,8 @@ export default function NavBar() {
const logout = () => api.auth.logout(() => {
dispatch(actions.auth.logout());
dispatch(actions.users.syncDrift(false))
if (window.drift) window.drift.reset();
navigate('/');
});
const isSupporter = user?.isSilverSupporter || user?.isGoldSupporter;
@ -58,10 +60,10 @@ export default function NavBar() {
return (
<StyledNavBar>
<Container style={{display:'flex', justifyContent: 'space-between', alignItems: 'center'}}>
<Link to={user ? '/projects' : '/'}><img alt={`${utils.appName()} logo`} src={logo} className="logo" /></Link>
<Link to="/"><img alt={`${utils.appName()} logo`} src={logo} className="logo" /></Link>
<div style={{flex: 1}}>
<Menu secondary>
<Menu.Item className='above-mobile' as={Link} to='/projects' name='projects' active={location.pathname === '/projects'} />
<Menu.Item className='above-mobile' as={Link} to='/' name='home' active={location.pathname === '/'} />
<Menu.Item className='above-mobile' as={Link} to='/explore' name='explore' active={location.pathname === '/explore'} />
<Menu.Item className='above-mobile' active={location.pathname.startsWith('/groups')} name='Groups'>
<Dropdown pointing='top left' icon={null}
@ -113,7 +115,7 @@ export default function NavBar() {
<Menu.Menu position='right'>
{isAuthenticated && <>
<Menu.Item className='abovee-mobile'><SearchBar /></Menu.Item>
<Menu.Item className='above-mobile'><SearchBar /></Menu.Item>
<Dropdown direction="left" pointing="top right" icon={null} style={{ marginTop: 10}}
trigger={<UserChip user={user} withoutLink avatarOnly />}
>
@ -126,8 +128,7 @@ export default function NavBar() {
{user?.isGoldSupporter && <Dropdown.Header><SupporterBadge type='gold' /></Dropdown.Header>}
{user?.isSilverSupporter && !user?.isGoldSupporter && <Dropdown.Header><SupporterBadge type='silver' /></Dropdown.Header>}
<Dropdown.Divider />
<Link to="/projects" className="item">My projects</Link>
<Link to="/explore" className="item">Explore</Link>
<Link to="/" className="item">Projects</Link>
{user &&<Link to={`/${user.username}`} className="item">Profile</Link>}
<Link to="/settings" className="item">Settings</Link>
<Dropdown.Divider />

View File

@ -9,7 +9,6 @@ import actions from '../../actions';
import api from '../../api';
import utils from '../../utils/utils.js';
import LoginNeeded from '../includes/LoginNeeded';
import UserChip from '../includes/UserChip';
import HelpLink from '../includes/HelpLink';
import ProjectCard from '../includes/ProjectCard';
@ -132,21 +131,15 @@ function Home() {
<Grid.Column computer={11} className='joyride-projects'>
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
<h2><Icon name='book' /> Your projects</h2>
{user &&
<div><Button className='joyride-createProject' as={Link} to="/projects/new" color='teal' content='Create a project' icon='plus' /></div>
}
<div><Button className='joyride-createProject' as={Link} to="/projects/new" color='teal' content='Create a project' icon='plus' /></div>
</div>
<p>Projects contain the patterns and files that make up your creations.
<HelpLink className='joyride-help' link={`/docs/projects`} text='Learn more about projects' marginLeft/>
</p>
<Divider hidden />
{!user &&
<LoginNeeded />
}
{user && loadingProjects && !projects?.length &&
{loadingProjects && !projects?.length &&
<Card.Group itemsPerRow={2} stackable>
<PatternLoader isCompact count={3} />
</Card.Group>

View File

@ -9,7 +9,6 @@ import api from '../../../api';
import utils from '../../../utils/utils.js';
import HelpLink from '../../includes/HelpLink';
import LoginNeeded from '../../includes/LoginNeeded';
function NewGroup() {
const navigate = useNavigate();
@ -34,7 +33,12 @@ function NewGroup() {
}
if (!user) {
return (<LoginNeeded />);
return (
<Container style={{marginTop: 40}}>
<h1>Login required</h1>
<p>You need to have a {utils.appName()} account in order to create a group. Please register or login first.</p>
</Container>
);
}
return (

View File

@ -114,7 +114,7 @@ function ObjectList({ compact }) {
<div style={{flex: 1, marginLeft: 5}}>
<h3 style={{fontSize: 13, marginBottom: 0, wordBreak: 'break-all'}}>{object.name}</h3>
<Label size='mini' rounded>
{object.type === 'pattern' && <><Icon name='pencil' /> Weaving pattern</>}
{object.type === 'pattern' && <><Icon name='pencil' /> WIF pattern</>}
{object.type === 'file' &&
(object.isImage ? <><Icon name='image' /> Image</> : <><Icon name='file outline' /> File</>)
}

View File

@ -83,6 +83,27 @@ function ObjectViewer() {
});
}
const downloadDrawdownImage = (object) => {
const element = document.createElement('a');
element.setAttribute('href', object.previewUrl);
element.setAttribute('download', `${object.name.replace(/ /g, '_')}-drawdown.png`);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
toast.info('The file has been downloaded');
}
const downloadPatternImage = (object) => {
const element = document.createElement('a');
element.setAttribute('href', object.patternImage);
element.setAttribute('download', `${object.name.replace(/ /g, '_')}-pattern.png`);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
toast.info('The file has been downloaded');
}
const copyPattern = (project) => {
api.objects.copyTo(object._id, project._id, newObject => {
window.location = `/${project.fullName}/${newObject._id}`;
@ -104,15 +125,15 @@ function ObjectViewer() {
<div style={{ display: 'flex', justifyContent: 'end' }}>
{object.type === 'pattern' && (utils.canEditProject(user, project) || project.openSource || object.previewUrl) && <>
<Dropdown direction='left' icon={null} trigger={<Button size='tiny' secondary icon='download' content='Download pattern' loading={downloading} disabled={downloading}/>}>
<Dropdown direction='left' icon={null} trigger={<Button size='small' secondary icon='download' content='Download pattern' loading={downloading} disabled={downloading}/>}>
<Dropdown.Menu>
{object.previewUrl &&
<Dropdown.Item onClick={e => utils.downloadDrawdownImage(object)} content='Download drawdown as an image' icon='file outline' />
<Dropdown.Item onClick={e => downloadDrawdownImage(object)} content='Download drawdown as an image' icon='file outline' />
}
{(utils.canEditProject(user, project) || project.openSource) &&
<React.Fragment>
{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 => downloadPatternImage(object)}/>
}
<Dropdown.Divider />
<Dropdown.Item onClick={e => downloadWif(object)} content="Download pattern in WIF format" icon="text file" />
@ -122,7 +143,7 @@ function ObjectViewer() {
</Dropdown>
{user &&
<Dropdown direction='left' icon={null} trigger={<Button size="tiny" icon="copy" secondary content="Copy to.." />}>
<Dropdown direction='left' icon={null} trigger={<Button size="small" icon="copy" secondary content="Copy to.." />}>
<Dropdown.Menu>
<Dropdown.Header>Select a project to copy this pattern to</Dropdown.Header>
{myProjects?.map(myProject => <Dropdown.Item content={myProject.name} onClick={e => copyPattern(myProject)} />)}
@ -135,17 +156,15 @@ function ObjectViewer() {
{utils.canEditProject(user, project) &&
<>
{object.type === 'pattern'
&& <Button size="tiny" icon="pencil" primary content="Edit pattern" as={Link} to={`/${fullProjectPath}/${object._id}/edit`} />
&& <Button size="small" icon="pencil" primary content="Edit pattern" as={Link} to={`/${fullProjectPath}/${object._id}/edit`} />
}
<Dropdown
icon={null}
direction='left'
trigger={<Button size="tiny" icon="cogs" content="Options" />}
trigger={<Button size="small" icon="cogs" content="Options" />}
>
<Dropdown.Menu>
{object.type === 'pattern'
&& <Dropdown.Item onClick={e => regeneratePreview(object)} content="Regenerate preview" icon="refresh" />
}
<Dropdown.Item onClick={e => regeneratePreview(object)} content="Regenerate preview" icon="refresh" />
<Dropdown.Item onClick={e => deleteObject(object)} content="Delete" icon="trash" />
</Dropdown.Menu>
</Dropdown>

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Helmet } from 'react-helmet';
import { Message, Form, TextArea, Container, Button, Icon, Grid, Card, Breadcrumb } from 'semantic-ui-react';
import { Message, Form, TextArea, Container, Button, Icon, Grid, Card } from 'semantic-ui-react';
import { Outlet, Link, useParams, useLocation } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import utils from '../../../utils/utils.js';
@ -14,14 +14,13 @@ import FormattedMessage from '../../includes/FormattedMessage';
function Project() {
const [descriptionExpanded, setDescriptionExpanded] = useState(false);
const { username, projectPath, objectId } = useParams();
const { username, projectPath } = useParams();
const dispatch = useDispatch();
const location = useLocation();
const { user, project, fullName, errorMessage, editingDescription, object } = useSelector(state => {
const { user, project, fullName, errorMessage, editingDescription } = useSelector(state => {
const project = state.projects.projects.filter(p => p.path === projectPath && p.owner && p.owner.username === username)[0];
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const object = state.objects.objects.filter(o => o._id === objectId)[0];
return { user, project, object, fullName: `${username}/${projectPath}`, errorMessage: state.projects.errorMessage, editingDescription: state.projects.editingDescription };
return { user, project, fullName: `${username}/${projectPath}`, errorMessage: state.projects.errorMessage, editingDescription: state.projects.editingDescription };
});
useEffect(() => {
@ -56,27 +55,16 @@ function Project() {
&& (
<div>
{wideBody() && project.owner &&
<div style={{display: 'flex', alignItems: 'center'}}>
<>
<h3 style={{ marginBottom: 0 }}>
{project.name}
<Button as={Link} size='tiny' style={{marginLeft: 10}} to={`/${project.fullName}`} basic secondary icon='arrow left' content='Back to project' />
</h3>
<UserChip user={project.owner} />
<Breadcrumb size='large' style={{marginLeft: 20}}>
<Breadcrumb.Section as={Link} to={`/${project.fullName}`}>{project.name}</Breadcrumb.Section>
{location.pathname === `/${project.fullName}/settings` && <>
<Breadcrumb.Divider icon='right chevron' />
<Breadcrumb.Section>Settings</Breadcrumb.Section>
</>}
{object && <>
<Breadcrumb.Divider icon='right chevron' />
<Breadcrumb.Section as={Link} to={`/${project.fullName}/${object._id}`}>{object.name}</Breadcrumb.Section>
{location.pathname === `/${project.fullName}/${object._id}/edit` && <>
<Breadcrumb.Divider icon='right chevron' />
<Breadcrumb.Section>Edit</Breadcrumb.Section>
</>}
</>}
</Breadcrumb>
</div>
</>
}
<Grid stackable style={{marginTop: 10}}>
<Grid stackable style={{marginTop: 30}}>
{ !wideBody() && (
<Grid.Column computer={4} tablet={6}>
<Card fluid raised>

View File

@ -1,13 +1,12 @@
import React, { useState, useEffect } from 'react';
import { Modal, Button } from 'semantic-ui-react';
import { Helmet } from 'react-helmet';
import { useSelector, useDispatch } from 'react-redux';
import { useParams } from 'react-router-dom';
import { useBlocker } from 'react-router';
import { toast } from 'react-toastify';
import styled from 'styled-components';
import Tour from '../../../includes/Tour';
import ElementPan from '../../../includes/ElementPan';
import HelpLink from '../../../includes/HelpLink';
import Tour, { ReRunTour } from '../../../includes/Tour';
import util from '../../../../utils/utils.js';
import Warp from './Warp';
@ -40,17 +39,10 @@ function Draft() {
useEffect(() => {
api.objects.get(objectId, (o) => {
if (!o.pattern.baseSize) o.pattern.baseSize = 10;
dispatch(actions.objects.receive(o));
setObject(o);
setPattern(o.pattern);
setTimeout(() => generatePreviews(o), 1000);
});
}, [objectId]);
const blocker = useBlocker( ({ currentLocation, nextLocation }) =>
unsaved &&
currentLocation.pathname !== nextLocation.pathname
);
const updateObject = (update) => {
setObject(Object.assign({}, object, update));
@ -62,21 +54,14 @@ function Draft() {
setPattern(Object.assign({}, pattern, newPattern));
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();
util.generatePatternPreview(object, previewUrl => {
dispatch(actions.objects.update(objectId, 'previewUrl', previewUrl));
});
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));
@ -92,46 +77,40 @@ function Draft() {
const { warp, weft, tieups, baseSize } = pattern;
const cellStyle = { width: `${baseSize || 10}px`, height: `${baseSize || 10}px` };
return (
<div style={{position: 'relative'}}>
<div>
<Helmet title={`${name || 'Weaving Draft'}`} />
<Tools warp={warp} weft={weft} object={object} pattern={pattern} updateObject={updateObject} updatePattern={updatePattern} saveObject={saveObject} baseSize={baseSize} unsaved={unsaved} saving={saving}/>
<div style={{overflow: 'hidden', zIndex: 10}}>
<ElementPan
disabled={!(editor?.tool === 'pan')}
startX={5000}
startY={0}
>
<StyledPattern
style={{
width: `${warp.threading?.length * baseSize + weft.treadles * baseSize + 20}px`,
height: `${warp.shafts * baseSize + weft.treadling?.length * baseSize + 20}px`
}}
>
<Warp baseSize={baseSize} cellStyle={cellStyle} warp={warp} weft={weft} updatePattern={updatePattern} />
<Weft cellStyle={cellStyle} warp={warp} weft={weft} baseSize={baseSize} updatePattern={updatePattern} />
<Tieups cellStyle={cellStyle} warp={warp} weft={weft} tieups={tieups} updatePattern={updatePattern} baseSize={baseSize}/>
<Drawdown warp={warp} weft={weft} tieups={tieups} baseSize={baseSize} />
<Tour id='pattern' run={true} />
<div style={{display: 'flex'}}>
<div style={{flex: 1, overflow: 'hidden'}}>
<ElementPan
disabled={!(editor?.tool === 'pan')}
startX={5000}
startY={0}
>
<StyledPattern
style={{
width: `${warp.threading?.length * baseSize + weft.treadles * baseSize + 20}px`,
height: '1000px', // `${warp.shafts * baseSize + weft.threads * baseSize + 20}px`
}}
>
<Warp baseSize={baseSize} cellStyle={cellStyle} warp={warp} weft={weft} updatePattern={updatePattern} />
<Weft cellStyle={cellStyle} warp={warp} weft={weft} baseSize={baseSize} updatePattern={updatePattern} />
<Tieups cellStyle={cellStyle} warp={warp} weft={weft} tieups={tieups} updatePattern={updatePattern} baseSize={baseSize}/>
<Drawdown warp={warp} weft={weft} tieups={tieups} baseSize={baseSize} />
</StyledPattern>
</ElementPan>
</div>
<div style={{width: 300, marginLeft: 20}}>
<HelpLink className='joyride-help' link={`/docs/patterns#using-the-pattern-editor`} marginBottom/>
<ReRunTour id='pattern' />
<Tools warp={warp} weft={weft} object={object} pattern={pattern} updateObject={updateObject} updatePattern={updatePattern} saveObject={saveObject} baseSize={baseSize} unsaved={unsaved} saving={saving}/>
</div>
</StyledPattern>
</ElementPan>
</div>
<Modal open={blocker.state === "blocked"} basic size="small">
<Modal.Header content='Your pattern has not been saved' />
<Modal.Content>
Would you like to save your draft before leaving?
</Modal.Content>
<Modal.Actions>
<Button basic color="red" inverted onClick={() => blocker.proceed()}>
Leave without saving
</Button>
<Button color="green" inverted onClick={() => blocker.reset()}>
Return to editor
</Button>
</Modal.Actions>
</Modal>
</div>
);
}

View File

@ -25,24 +25,54 @@ function DraftPreview({ object }) {
setLoading(false);
if (o.pattern && o.pattern.warp) {
setPattern(o.pattern);
// Generate images
setTimeout(() => {
// Generate the preview if not yet set (e.g. if from uploaded WIF)
if (!o.previewUrl) {
// Generate the preview if not yet set (e.g. if from uploaded WIF)
if (!o.previewUrl) {
setTimeout(() => {
util.generatePatternPreview(object, previewUrl => {
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));
});
}, 1000);
}, 1000);
}
}
}, err => setLoading(false));
}, [dispatch, objectId]);
const unifyCanvas = useCallback(() => {
if (!pattern) return;
const { warp, weft } = pattern;
setTimeout(() => {
const baseSize = 6;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = warp.threading?.length * baseSize + weft.treadles * baseSize + 20;
canvas.height = warp.shafts * baseSize + weft.treadling?.length * baseSize + 20;
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'black';
const warpCanvas = document.querySelector(`.preview-${objectId} .warp-threads`);
const warpColourwayCanvas = document.querySelector(`.preview-${objectId} .warp-colourway`);
const weftCanvas = document.querySelector(`.preview-${objectId} .weft-threads`);
const weftColourwayCanvas = document.querySelector(`.preview-${objectId} .weft-colourway`);
const drawdownCanvas = document.querySelector(`.preview-${objectId} .drawdown`);
const tieupsCanvas = document.querySelector(`.preview-${objectId} .tieups`);
if (warpCanvas) {
ctx.drawImage(warpColourwayCanvas, canvas.width - warpCanvas.width - weft.treadles * baseSize - 20, 0);
ctx.drawImage(warpCanvas, canvas.width - warpCanvas.width - weft.treadles * baseSize - 20, 10);
ctx.drawImage(weftCanvas, canvas.width - 10 - weft.treadles * baseSize, warp.shafts * baseSize + 20);
ctx.drawImage(weftColourwayCanvas, canvas.width - 10, warp.shafts * baseSize + 20);
ctx.drawImage(tieupsCanvas, canvas.width - weft.treadles * baseSize - 10, 10);
ctx.drawImage(drawdownCanvas, canvas.width - drawdownCanvas.width - weft.treadles * baseSize - 20, warp.shafts * baseSize + 20);
setTimeout(() => {
const im = canvas.toDataURL('image/png')
if (im?.length > 20)
dispatch(actions.objects.update(objectId, 'patternImage', im));
}, 500);
}
}, 500);
}, [dispatch, objectId, pattern]);
useEffect(() => unifyCanvas(), [unifyCanvas]);
if (loading) return <Loader active />;
if (!pattern) return null;
const { warp, weft, tieups } = pattern;

View File

@ -3,9 +3,9 @@ import { useSelector } from 'react-redux';
import styled from 'styled-components';
import utils from '../../../../utils/utils';
const DrawdownCanvas = styled.canvas`
position: absolute;
border: 1px dashed rgb(70,70,70);
const StyledDrawdown = styled.canvas`
position:absolute;
border:1px dashed rgb(70,70,70);
top: ${props => (props.warp.shafts * props.baseSize) + 20}px;
right: ${props => (props.weft.treadles * props.baseSize) + 20}px;
`;
@ -17,7 +17,6 @@ function Drawdown({ baseSize, warp, weft, tieups }) {
const drawdownRef = useRef();
useEffect(() => paintDrawdown());
const { editor } = useSelector(state => ({ editor: state.objects.editor }));
const { viewingBack } = editor;
const getSquare = (thread, size, colour) => {
const { view } = editor;
@ -76,11 +75,7 @@ function Drawdown({ baseSize, warp, weft, tieups }) {
if (proceed) {
const weftColour = utils.rgb(weft.treadling[tread].colour || weft.defaultColour);
const warpColour = utils.rgb(warp.threading[thread].colour || warp.defaultColour);
let threadType = tieup?.filter(t => t <= warp.shafts).indexOf(shaft) > -1 ? 'warp' : 'weft';
if (viewingBack) {
if (threadType === 'warp') threadType = 'weft';
else threadType = 'warp';
}
const threadType = tieup && tieup.filter(t => t <= warp.shafts).indexOf(shaft) > -1 ? 'warp' : 'weft';
const square = getSquare(threadType, baseSize, threadType === 'warp' ? warpColour : weftColour);
ctx.drawImage(square, canvas.width - (baseSize * (thread + 1)), tread * baseSize);
@ -92,7 +87,7 @@ function Drawdown({ baseSize, warp, weft, tieups }) {
const warpThreads = warp.threading?.length || 0;
const weftThreads = weft.treadling?.length || 0;
return (
<DrawdownCanvas ref={drawdownRef} className="drawdown joyride-drawdown"
<StyledDrawdown ref={drawdownRef} className="drawdown joyride-drawdown"
width={warpThreads * baseSize}
height={weftThreads * baseSize}
weft={weft} warp={warp} baseSize={baseSize}

View File

@ -1,17 +1,15 @@
import React, { useEffect, useRef } from 'react';
import styled from 'styled-components';
import { useSelector } from 'react-redux';
const StyledTieups = styled.canvas`
position: absolute;
top: 10px;
right: 10px;
position:absolute;
top:10px;
right:10px;
`;
function Tieups({ cellStyle, warp, weft, tieups, updatePattern, baseSize }) {
useEffect(() => paintTieups());
const tieupRef = useRef(null);
const { editor } = useSelector(state => ({ editor: state.objects.editor }));
const fillUpTo = (t, limit) => {
let i = t.length;

View File

@ -1,15 +1,13 @@
import React, { useState, useEffect } from 'react';
import {
Confirm, Header, Segment, Accordion, Grid, Icon, Input, Button, Popup, Checkbox, Dropdown
Confirm, Header, Select, Segment, Accordion, Grid, Icon, Input, Button, Popup, Checkbox
} from 'semantic-ui-react';
import { useSelector, useDispatch } from 'react-redux';
import { useNavigate, useParams, Link } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
import { toast } from 'react-toastify';
import { SketchPicker } from 'react-color';
import Slider from 'rc-slider';
import styled from 'styled-components';
import HelpLink from '../../../includes/HelpLink';
import Tour, { ReRunTour } from '../../../includes/Tour';
import 'rc-slider/assets/index.css';
@ -17,22 +15,15 @@ import utils from '../../../../utils/utils.js';
import actions from '../../../../actions';
import api from '../../../../api';
const VIEWS = [
{ key: 1, value: 'interlacement', text: 'Interlacement' },
{ key: 2, value: 'colour', text: 'Colour only' },
{ key: 3, value: 'warp', text: 'Warp view' },
{ key: 4, value: 'weft', text: 'Weft view' },
];
const ColourSquare = styled.div`
background-color: ${props => props.colour};
display: ${props => props.small ? 'block' : 'inline-block'};
display:inline-block;
position:relative;
box-shadow:0px 0px 3px rgba(0,0,0,0.1);
border-radius:2px;
width: ${props => props.small ? '8' : '15'}px;
height: ${props => props.small ? '8' : '15'}px;
margin: ${props => props.small ? '8' : '2'}px;
width:15px;
height:15px;
margin:2px;
cursor:pointer;
top:0px;
transition:top 0.1s;
@ -45,11 +36,6 @@ const ColourSquare = styled.div`
}
`;
const ToolLabel = styled.div`
font-size: small;
font-weight: bold;
`;
function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updatePattern, updateObject, saveObject }) {
const [activeDrawers, setActiveDrawers] = useState(['properties', 'drawing', 'palette']);
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
@ -58,15 +44,13 @@ function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updateP
const navigate = useNavigate();
const dispatch = useDispatch();
const { objectId, username, projectPath } = useParams();
const { colours } = pattern;
const { user, project, editor } = useSelector(state => {
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const { project, editor } = useSelector(state => {
let project = {};
state.projects.projects.forEach((p) => {
if (p.path === projectPath && p.owner && p.owner.username === username) project = p;
});
return { user, project, editor: state.objects.editor };
return { project, editor: state.objects.editor };
});
useEffect(() => {
@ -77,10 +61,6 @@ function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updateP
selected += weft?.treadling?.filter(t => t.isSelected)?.length;
setSelectedThreadCount(selected);
}, [pattern]);
useEffect(() => {
if (colours?.length && !editor.colour) setColour(colours[colours.length - 1]);
}, [colours]);
const enableTool = (tool) => {
dispatch(actions.objects.updateEditor({ tool, colour: editor.colour }));
@ -93,10 +73,6 @@ function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updateP
const setEditorView = (view) => {
dispatch(actions.objects.updateEditor({ view }));
};
const setEditorViewingBack = (viewingBack) => {
dispatch(actions.objects.updateEditor({ viewingBack }));
};
const setName = (event) => {
updateObject({ name: event.target.value });
@ -201,173 +177,138 @@ function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updateP
};
return (
<div className="joyride-tools" style={{position: 'sticky', top: 10, zIndex: 20}}>
<Segment>
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
{selectedThreadCount > 0 ?
<div>
<Header>{selectedThreadCount} threads selected</Header>
<Button size='small' basic onClick={deselectThreads}>De-select all</Button>
<Button size='small' color='orange' onClick={deleteSelectedThreads}>Delete threads</Button>
</div>
:
<div style={{display: 'flex', alignItems: 'end'}}>
<div>
<ToolLabel>View</ToolLabel>
<Popup hoverable on='click'
trigger={<Button color='blue' size="tiny" icon="zoom" />}
content={<div style={{width: 150}}>
<h4>Zoom</h4>
<Slider defaultValue={baseSize} min={5} max={13} step={1} onAfterChange={onZoomChange} />
</div>}
/>
<small><Dropdown text={`${VIEWS.find(v => v.value === editor.view)?.text} (${editor.viewingBack ? 'Back' : 'Front'})`} size='tiny'>
<Dropdown.Menu>
{VIEWS.map(view =>
<Dropdown.Item key={view.value} text={view.text} onClick={e => setEditorView(view.value)} />
)}
<Dropdown.Divider />
<Dropdown.Item onClick={() => setEditorViewingBack(false)}>Front{!editor.viewingBack && <Icon style={{marginLeft: 5}} name='check' />}</Dropdown.Item>
<Dropdown.Item onClick={() => setEditorViewingBack(true)}>Back {editor.viewingBack && <Icon style={{marginLeft: 5}} name='check' />}</Dropdown.Item>
</Dropdown.Menu>
</Dropdown></small>
</div>
<div style={{marginLeft: 10}}>
<ToolLabel>Tools & drawing</ToolLabel>
<Button.Group size="tiny">
<Button className='joyride-pan' data-tooltip="Pan (drag to move) pattern" color={editor.tool === 'pan' && 'blue'} size="tiny" icon onClick={() => enableTool('pan')}><Icon name="move" /></Button>
<Button data-tooltip="Select threads" color={editor.tool === 'select' && 'blue'} size="tiny" icon onClick={() => enableTool('select')}><Icon name="i cursor" /></Button>
<Button data-tooltip="Insert threads" color={editor.tool === 'insert' && 'blue'} size="tiny" icon onClick={() => enableTool('insert')}><Icon name="plus" /></Button>
<Button className='joyride-colour' data-tooltip="Apply thread colour" color={editor.tool === 'colour' && 'blue'} size="tiny" icon onClick={() => enableTool('colour')}><Icon name="paint brush" /></Button>
<Button data-tooltip="Erase threads" color={editor.tool === 'eraser' && 'blue'} size="tiny" icon onClick={() => enableTool('eraser')}><Icon name="eraser" /></Button>
<Button className='joyride-straight' data-tooltip="Straight draw" color={editor.tool === 'straight' && 'blue'} size="tiny" icon onClick={() => enableTool('straight')}>/ /</Button>
<Button className='joyride-point' data-tooltip="Point draw" color={editor.tool === 'point' && 'blue'} size="tiny" icon onClick={() => enableTool('point')}><Icon name="chevron up" /></Button>
</Button.Group>
</div>
<div style={{marginLeft: 10}}>
<div style={{display: 'flex', 'alignItems': 'end'}}>
<ToolLabel>Colour</ToolLabel>
<div><ColourSquare small colour={utils.rgb(editor.colour)} style={{top: 4, marginLeft: 10}} /></div>
</div>
<Button.Group size="tiny">
<Popup hoverable on="click"
trigger={<Button style={{marginLeft: 5}} size="tiny" icon="tint" style={{color: utils.rgb(editor.colour)}} />}
content={<div style={{width: 150}}>
{pattern.colours && pattern.colours.map(colour =>
<ColourSquare key={colour} colour={utils.rgb(colour)} onClick={() => setColour(colour)} />
)}
</div>}
<div className="pattern-toolbox joyride-tools">
{selectedThreadCount > 0 &&
<Segment attached="top">
<Header>{selectedThreadCount} threads selected</Header>
<Button basic onClick={deselectThreads}>De-select all</Button>
<Button color='orange' onClick={deleteSelectedThreads}>Delete threads</Button>
</Segment>
}
{unsaved &&
<Segment attached>
<Button fluid color="teal" icon="save" content="Save pattern" onClick={() => saveObject(/*this.refs.canvas*/)} loading={saving}/>
<Button style={{marginTop: 5}} fluid icon='refresh' content='Undo changes' onClick={revertChanges} />
</Segment>
}
<Segment attached>
<Accordion fluid>
<Accordion.Title active={drawerIsActive('view')} onClick={e => activateDrawer('view')}>
<Icon name="dropdown" /> View
</Accordion.Title>
<Accordion.Content active={drawerIsActive('view')}>
<small>Drawdown view</small>
<Select
size="tiny"
fluid
value={editor.view}
onChange={(e, s) => setEditorView(s.value)}
style={{ fontSize: '11px' }}
options={[
{ key: 1, value: 'interlacement', text: 'Interlacement' },
{ key: 2, value: 'colour', text: 'Colour only' },
{ key: 3, value: 'warp', text: 'Warp view' },
{ key: 4, value: 'weft', text: 'Weft view' },
]}
/>
<div style={{ marginTop: '5px' }} />
<small>Zoom</small>
<Slider defaultValue={baseSize} min={5} max={13} step={1} onAfterChange={onZoomChange} />
</Accordion.Content>
<Accordion.Title active={drawerIsActive('properties')} onClick={e => activateDrawer('properties')}>
<Icon name="dropdown" /> Properties
</Accordion.Title>
<Accordion.Content active={drawerIsActive('properties')}>
<small>Name</small>
<Input type="text" size="small" fluid style={{ marginBottom: '5px' }} value={object.name} onChange={setName} />
<Grid columns={2}>
<Grid.Row className='joyride-threads'>
<Grid.Column>
<small>Shafts</small>
<Input fluid type="number" value={warp.shafts} onKeyDown={e => false} onChange={setShafts} size="mini" />
</Grid.Column>
<Grid.Column>
<small>Treadles</small>
<Input fluid type="number" value={weft.treadles} onKeyDown={e => false} onChange={setTreadles} size="mini" />
</Grid.Column>
</Grid.Row>
<Grid.Row style={{paddingTop: 0}}>
<Grid.Column>
<small>Width (warp threads)</small>
<Input fluid readOnly value={warp.threading?.length || 0} size="mini"
action={{icon: 'edit', onClick: changeWidth}}
/>
<Popup hoverable on='click'
trigger={<Button color='blue' size='mini' icon='add' />}
content={
<div style={{padding: 3}}>
<SketchPicker color={newColour} onChangeComplete={c => setNewColour(c.rgb)} />
<Button size='sm' style={{marginTop: 10}} onClick={e => {
const { r, g, b } = newColour;
const newColours = Object.assign([], pattern.colours);
newColours.push(`${r},${g},${b}`);
updatePattern({ colours: newColours })
}}>Add colour to palette</Button>
</div>
}
</Grid.Column>
<Grid.Column>
<small>Height (weft threads)</small>
<Input fluid readOnly value={weft.treadling?.length || 0} size="mini"
action={{icon: 'edit', onClick: changeHeight}}
/>
</Button.Group>
</div>
</div>
}
<div>
<div>
<Popup hoverable on='click' position='top right'
trigger={<Button color='blue' style={{marginLeft: 5}} size='tiny' content='Properties' />}
</Grid.Column>
</Grid.Row>
</Grid>
<Popup
content='Add new threads to the warp and weft as you edit near the end'
trigger={<Checkbox checked={editor.autoExtend ?? true} onChange={setAutomaticallyExtend} label='Auto-extend warp and weft' style={{marginTop: 10}} />}
/>
</Accordion.Content>
<Accordion.Title active={drawerIsActive('drawing')} onClick={e => activateDrawer('drawing')}>
<Icon name="dropdown" /> Tools
</Accordion.Title>
<Accordion.Content active={drawerIsActive('drawing')}>
<Button.Group fluid>
<Button className='joyride-pan' data-tooltip="Pan (drag to move) pattern" color={editor.tool === 'pan' && 'blue'} size="tiny" icon onClick={() => enableTool('pan')}><Icon name="move" /></Button>
<Button data-tooltip="Select threads" color={editor.tool === 'select' && 'blue'} size="tiny" icon onClick={() => enableTool('select')}><Icon name="i cursor" /></Button>
<Button data-tooltip="Insert threads" color={editor.tool === 'insert' && 'blue'} size="tiny" icon onClick={() => enableTool('insert')}><Icon name="plus" /></Button>
<Button className='joyride-colour' data-tooltip="Apply thread colour" color={editor.tool === 'colour' && 'blue'} size="tiny" icon onClick={() => enableTool('colour')}><Icon name="paint brush" /></Button>
<Button className='joyride-straight' data-tooltip="Straight draw" color={editor.tool === 'straight' && 'blue'} size="tiny" icon onClick={() => enableTool('straight')}>/ /</Button>
<Button className='joyride-point' data-tooltip="Point draw" color={editor.tool === 'point' && 'blue'} size="tiny" icon onClick={() => enableTool('point')}><Icon name="chevron up" /></Button>
</Button.Group>
</Accordion.Content>
<Accordion.Title active={drawerIsActive('palette')} onClick={e => activateDrawer('palette')}>
<Icon name="dropdown" /> Palette
<ColourSquare colour={utils.rgb(editor.colour)} style={{top: 4, marginLeft: 10}} />
</Accordion.Title>
<Accordion.Content active={drawerIsActive('palette')}>
{pattern.colours && pattern.colours.map(colour =>
<ColourSquare key={colour} colour={utils.rgb(colour)} onClick={() => setColour(colour)} />
)}
<div style={{marginTop: 10}}>
<Popup
trigger={<Button size='mini'><Icon name='add' /> Colour</Button>}
on='click' hoverable
content={
<div style={{padding: 3, width: 300}}>
<small>Name</small>
<Input type="text" size="small" fluid style={{ marginBottom: '5px' }} value={object.name} onChange={setName} />
<Grid columns={2}>
<Grid.Row className='joyride-threads'>
<Grid.Column>
<small>Shafts</small>
<Input fluid type="number" value={warp.shafts} onKeyDown={e => false} onChange={setShafts} size="mini" />
</Grid.Column>
<Grid.Column>
<small>Treadles</small>
<Input fluid type="number" value={weft.treadles} onKeyDown={e => false} onChange={setTreadles} size="mini" />
</Grid.Column>
</Grid.Row>
<Grid.Row style={{paddingTop: 0}}>
<Grid.Column>
<small>Width (warp threads)</small>
<Input fluid readOnly value={warp.threading?.length || 0} size="mini"
action={{icon: 'edit', onClick: changeWidth}}
/>
</Grid.Column>
<Grid.Column>
<small>Height (weft threads)</small>
<Input fluid readOnly value={weft.treadling?.length || 0} size="mini"
action={{icon: 'edit', onClick: changeHeight}}
/>
</Grid.Column>
</Grid.Row>
</Grid>
<Popup
content='Add new threads to the warp and weft as you edit near the end'
trigger={<Checkbox checked={editor.autoExtend ?? true} onChange={setAutomaticallyExtend} label='Auto-extend warp and weft' style={{marginTop: 10}} />}
/>
<div style={{marginTop: 20}}>
<Button basic color='red' fluid onClick={e => setDeleteModalOpen(true)}>Delete pattern</Button>
</div>
<div style={{padding: 5}}>
<SketchPicker color={newColour} onChangeComplete={c => setNewColour(c.rgb)} />
<Button size='sm' style={{marginTop: 10}} onClick={e => {
const { r, g, b } = newColour;
const newColours = Object.assign([], pattern.colours);
newColours.push(`${r},${g},${b}`);
updatePattern({ colours: newColours })
}}>Add colour to palette</Button>
</div>
}
/>
<Confirm
open={deleteModalOpen}
content="Really delete this pattern?"
onCancel={e => setDeleteModalOpen(false)}
onConfirm={deleteObject}
/>
<Dropdown icon={null} direction='left' disabled={unsaved}
trigger={<Button color='blue' size='tiny' icon='download' style={{marginLeft: 5}} />}
>
<Dropdown.Menu>
{object.previewUrl &&
<Dropdown.Item onClick={e => utils.downloadDrawdownImage(object)} content='Download drawdown as an image' icon='file outline' />
}
{(utils.canEditProject(user, project) || project.openSource) &&
<>
{object.patternImage &&
<Dropdown.Item icon='file outline' content='Download complete pattern as an image' onClick={e => utils.downloadPatternImage(object)}/>
}
<Dropdown.Divider />
<Dropdown.Item onClick={e => utils.downloadWif(object)} content="Download pattern in WIF format" icon="text file" />
</>
}
</Dropdown.Menu>
</Dropdown>
<Dropdown style={{marginLeft: 5}} icon={null} direction='left'
trigger={<Button basic size='tiny' icon='help' />}
>
<Dropdown.Menu>
<Dropdown.Item as={Link} to={`/docs/patterns#using-the-pattern-editor`}target="_blank">Documentation</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</div>
</Accordion.Content>
<div style={{marginTop: 5, textAlign: 'right'}}>
<Button.Group size="tiny">
<Button size="tiny" color="teal" icon="save" content="Save" onClick={() => saveObject()} loading={saving} disabled={!unsaved} />
<Button size="tiny" color="orange" icon='refresh' content='Revert' onClick={revertChanges} disabled={!unsaved} />
</Button.Group>
</div>
</div>
</div>
<Accordion.Title active={drawerIsActive('advanced')} onClick={e => activateDrawer('advanced')}>
<Icon name="dropdown" /> Advanced
</Accordion.Title>
<Accordion.Content active={drawerIsActive('advanced')}>
<Button size="small" basic color="red" fluid onClick={e => setDeleteModalOpen(true)}>Delete pattern</Button>
<Confirm
open={deleteModalOpen}
content="Really delete this pattern?"
onCancel={e => setDeleteModalOpen(false)}
onConfirm={deleteObject}
/>
</Accordion.Content>
</Accordion>
</Segment>
</div>
);

View File

@ -3,8 +3,9 @@ import styled from 'styled-components';
import { useSelector } from 'react-redux';
import utils from '../../../../utils/utils.js';
const WarpContainer = styled.div`
const StyledWarp = styled.div`
top:0px;
right:0px;
position: absolute;
right: ${props => (props.treadles * props.baseSize) + 20}px;
height: ${props => (props.shafts * props.baseSize) + 40}px;
@ -16,24 +17,6 @@ const WarpContainer = styled.div`
}
`;
const WarpCanvas = styled.canvas`
position: absolute;
top: 10px;
right: 0px;
height: ${props => props.warp.shafts * props.baseSize}px;
width: ${props => props.warp.threading.length * props.baseSize}px;
border-radius: 4;
box-shadow: 0px 0px 10px rgba(0,0,0,0.15);
`;
const Colourway = styled.canvas`
position: absolute;
top: 0px;
right: 0px;
height: 10px;
width: ${props => props.warp.threading.length * props.baseSize}px;
`;
const squares = {};
const markers = {};
const selectedMarkers = {};
@ -164,10 +147,6 @@ function Warp({ baseSize, cellStyle, warp, weft, updatePattern }) {
if (thread >= warp.threading.length) fillUpTo(newWarp, thread);
newWarp.threading[thread].colour = editor.colour;
}
if (editor.tool === 'eraser') {
if (thread >= warp.threading.length) fillUpTo(newWarp, thread);
newWarp.threading[thread].shaft = 0;
}
if (editor.tool === 'straight') {
while (x <= hX && x >= lX) {
if (x >= warp.threading.length || warp.threading.length - x < 5) fillUpTo(newWarp, x + 5);
@ -204,13 +183,6 @@ function Warp({ baseSize, cellStyle, warp, weft, updatePattern }) {
warpThread.shaft = warpThread.shaft === shaft ? 0 : shaft;
updatePattern({ warp: newWarp });
}
if (editor.tool === 'eraser') {
const newWarp = Object.assign({}, warp);
if (thread > warp.threading.length || warp.threading.length - thread < 5) fillUpTo(newWarp, thread + 5);
const warpThread = newWarp.threading[thread];
warpThread.shaft = 0;
updatePattern({ warp: newWarp });
}
if (editor.tool === 'select') {
const newWarp = Object.assign({}, warp);
const warpThread = newWarp.threading[thread];
@ -334,22 +306,31 @@ function Warp({ baseSize, cellStyle, warp, weft, updatePattern }) {
};
return (
<WarpContainer treadles={weft.treadles} shafts={warp.shafts} baseSize={baseSize} tool={tool}>
<Colourway className='warp-colourway joyride-warpColourway' ref={colourwayRef} width={warp.threading.length * baseSize} height={10} warp={warp} baseSize={baseSize}
<StyledWarp treadles={weft.treadles} shafts={warp.shafts} baseSize={baseSize} tool={tool}>
<canvas className='warp-colourway joyride-warpColourway' ref={colourwayRef} width={warp.threading.length * baseSize} height={10}
style={{
position: 'absolute', top: 0, right: 0, height: 10, width: warp.threading.length * baseSize,
}}
onClick={mouseClickColourway}
onMouseDown={mouseDownColourway}
onMouseMove={mouseMoveColourway}
onMouseUp={mouseUpColourway}
onMouseLeave={mouseUpColourway}
/>
<WarpCanvas className='warp-threads joyride-warp' ref={warpRef} width={warp.threading.length * baseSize} height={warp.shafts * baseSize} warp={warp} baseSize={baseSize}
<canvas className='warp-threads joyride-warp' ref={warpRef} width={warp.threading.length * baseSize} height={warp.shafts * baseSize}
style={{
position: 'absolute', top: 10, right: 0,
height: warp.shafts * baseSize,
width: warp.threading.length * baseSize, borderRadius: 4,
boxShadow: '0px 0px 10px rgba(0,0,0,0.15)',
}}
onClick={click}
onMouseDown={mouseDown}
onMouseMove={mouseMove}
onMouseUp={mouseUp}
onMouseLeave={mouseUp}
/>
</WarpContainer>
</StyledWarp>
);
}

View File

@ -3,7 +3,9 @@ import styled from 'styled-components';
import { useSelector } from 'react-redux';
import utils from '../../../../utils/utils.js';
const WeftContainer = styled.div`
const StyledWeft = styled.div`
top:0px;
right:0px;
position: absolute;
top: ${props => (props.shafts * props.baseSize) + 20}px;
right: 0;
@ -17,24 +19,6 @@ const WeftContainer = styled.div`
}
`;
const WeftCanvas = styled.canvas`
position: absolute;
top: 0px;
right: 10px;
height: ${props => props.weft.treadling?.length * props.baseSize}px;
width: ${props => props.weft.treadles * props.baseSize}px;
border-radius: 4;
box-shadow: 0px 0px 10px rgba(0,0,0,0.15);
`;
const Colourway = styled.canvas`
position: absolute;
top: 0px;
right: 0px;
width: 10px;
height: ${props => props.weft.treadling?.length * props.baseSize}px;
`;
const squares = {};
const markers = {};
let dragging = false;
@ -164,10 +148,6 @@ function Weft({ cellStyle, warp, weft, baseSize, updatePattern }) {
if ((thread - 1) >= weft.treadling.length) fillUpTo(newWeft, (thread - 1));
newWeft.treadling[thread - 1].colour = editor.colour;
}
if (editor.tool === 'eraser') {
if ((thread - 1) >= weft.treadling.length) fillUpTo(newWeft, (thread - 1));
newWeft.treadling[thread - 1].treadle = 0;
}
if (editor.tool === 'straight') {
while (y <= hY && y >= lY) {
if ((y - 1) >= weft.treadling.length || weft.treadling.length - y - 1 < 5) fillUpTo(newWeft, (y + 5));
@ -205,14 +185,6 @@ function Weft({ cellStyle, warp, weft, baseSize, updatePattern }) {
weftThread.treadle = weftThread.treadle === treadle ? 0 : treadle;
updatePattern({ weft: newWeft });
}
if (editor.tool === 'eraser') {
treadle += 1;
const newWeft = Object.assign({}, weft);
if (thread >= newWeft.treadling.length || newWeft.treadling.length - thread < 5) fillUpTo(newWeft, thread + 5);
const weftThread = newWeft.treadling[thread - 1];
weftThread.treadle = 0;
updatePattern({ weft: newWeft });
}
if (editor.tool === 'select') {
const newWeft = Object.assign({}, weft);
const weftThread = newWeft.treadling[thread - 1];
@ -337,22 +309,28 @@ function Weft({ cellStyle, warp, weft, baseSize, updatePattern }) {
const threadCount = weft.treadling?.length || 0;
return (
<WeftContainer baseSize={baseSize} treadles={weft.treadles} shafts={warp.shafts} threads={threadCount} tool={tool}>
<Colourway className='weft-colourway' ref={colourwayRef} width={10} height={threadCount * baseSize} weft={weft} baseSize={baseSize}
<StyledWeft baseSize={baseSize} treadles={weft.treadles} shafts={warp.shafts} threads={threadCount} tool={tool}>
<canvas className='weft-colourway' ref={colourwayRef} width={10} height={threadCount * baseSize}
style={{ position: 'absolute', top: 0, right: 0, width: 10, height: threadCount * baseSize}}
onClick={mouseClickColourway}
onMouseDown={mouseDownColourway}
onMouseMove={mouseMoveColourway}
onMouseUp={mouseUpColourway}
onMouseLeave={mouseUpColourway}
/>
<WeftCanvas className='weft-threads joyride-weft' ref={weftRef} width={weft.treadles * baseSize} height={threadCount * baseSize} weft={weft} baseSize={baseSize}
<canvas className='weft-threads joyride-weft' ref={weftRef} width={weft.treadles * baseSize} height={threadCount * baseSize}
style={{
position: 'absolute',
top: 0, right: 10, height: threadCount * baseSize, width: weft.treadles * baseSize,
borderRadius: 4, boxShadow: '0px 0px 10px rgba(0,0,0,0.15)',
}}
onClick={click}
onMouseDown={mouseDown}
onMouseMove={mouseMove}
onMouseUp={mouseUp}
onMouseLeave={mouseUp}
/>
</WeftContainer>
</StyledWeft>
);
}

View File

@ -1,7 +1,7 @@
import 'react-app-polyfill/ie9';
import React from 'react';
import { createRoot } from 'react-dom/client';
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
import { BrowserRouter } from 'react-router-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import * as Sentry from '@sentry/react';
@ -10,48 +10,7 @@ import 'react-toastify/dist/ReactToastify.css';
import 'pell/dist/pell.min.css';
import reducers from './reducers';
import App from './components/App';
import Home from './components/main/Home';
import MarketingHome from './components/marketing/Home';
import PrivacyPolicy from './components/marketing/PrivacyPolicy';
import TermsOfUse from './components//marketing/TermsOfUse';
import Login from './components/Login';
import ForgottenPassword from './components/ForgottenPassword';
import ResetPassword from './components/ResetPassword';
import Settings from './components/main/settings/Settings';
import SettingsIdentity from './components/main/settings/Identity';
import SettingsNotification from './components/main/settings/Notification';
import SettingsAccount from './components/main/settings/Account';
import Profile from './components/main/users/Profile';
import ProfileEdit from './components/main/users/EditProfile';
import ProfileProjects from './components/main/users/ProfileProjects';
import NewProject from './components/main/projects/New';
import Project from './components/main/projects/Project';
import ProjectObjects from './components/main/projects/ProjectObjects';
import ProjectSettings from './components/main/projects/Settings';
import ObjectDraft from './components/main/projects/objects/Draft';
import ObjectList from './components/main/projects/ObjectList';
import NewGroup from './components/main/groups/New';
import Group from './components/main/groups/Group';
import GroupFeed from './components/main/groups/Feed';
import GroupMembers from './components/main/groups/Members';
import GroupProjects from './components/main/groups/Projects';
import GroupSettings from './components/main/groups/Settings';
import Explore from './components/main/explore/Explore'
import Docs from './components/docs';
import DocsHome from './components/docs/Home';
import DocsDoc from './components/docs/Doc';
import Root from './components/main/root';
export const store = createStore(reducers);
@ -63,56 +22,12 @@ if (import.meta.env.VITE_SENTRY_DSN) {
});
}
const router = createBrowserRouter([
{
path: "/",
element: <App />,
children: [
{ path: "", element: <MarketingHome /> },
{ path: "root", element: <Root /> },
{ path: "explore", element: <Explore /> },
{ path: "privacy", element: <PrivacyPolicy /> },
{ path: "terms-of-use", element: <TermsOfUse /> },
{ path: "password/forgotten", element: <ForgottenPassword /> },
{ path: "password/reset", element: <ResetPassword /> },
{ path: "settings", element: <Settings />, children: [
{ path: "identity", element: <SettingsIdentity /> },
{ path: "notifications", element: <SettingsNotification /> },
{ path: "account", element: <SettingsAccount /> },
{ path: "", element: <SettingsIdentity /> },
] },
{ path: "projects/new", element: <NewProject /> },
{ path: "projects", element: <Home /> },
{ path: "groups/new", element: <NewGroup /> },
{ path: "groups/:id", element: <Group />, children: [
{ path: "feed", element: <GroupFeed /> },
{ path: "members", element: <GroupMembers /> },
{ path: "projects", element: <GroupProjects /> },
{ path: "settings", element: <GroupSettings /> },
{ path: "", element: <GroupFeed /> },
] },
{ path: "docs", element: <Docs />, children: [
{ path: ":doc", element: <DocsDoc /> },
{ path: "", element: <DocsHome /> },
] },
{ path: ":username/:projectPath", element: <Project />, children: [
{ path: "settings", element: <ProjectSettings /> },
{ path: ":objectId/edit", element: <ObjectDraft /> },
{ path: ":objectId", element: <ProjectObjects /> },
{ path: "", element: <ObjectList /> },
] },
{ path: ":username", element: <Profile />, children: [
{ path: "edit", element: <ProfileEdit /> },
{ path: "", element: <ProfileProjects /> },
] },
],
},
]);
const container = document.getElementById('root');
const root = createRoot(container);
root.render(
<Provider store={store}>
<RouterProvider router={router} />
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
);

View File

@ -7,7 +7,7 @@ const initialState = {
explorePage: 1,
comments: [],
selected: null,
editor: { tool: 'straight', colour: null, view: 'interlacement', autoExtend: true },
editor: { tool: 'straight', colour: 'orange', view: 'interlacement', autoExtend: true },
};
function objects(state = initialState, action) {

View File

@ -1,6 +1,8 @@
import actions from '../actions';
const initialState = {
driftReady: false,
syncedToDrift: false,
loading: false,
errorMessage: '',
users: [],
@ -8,6 +10,10 @@ const initialState = {
function users(state = initialState, action) {
switch (action.type) {
case actions.users.INIT_DRIFT:
return Object.assign({}, state, { driftReady: true });
case actions.users.SYNC_DRIFT:
return Object.assign({}, state, { syncedToDrift: action.synced ?? true });
case actions.users.REQUEST_USERS:
return Object.assign({}, state, { loading: true, errorMessage: '' });

View File

@ -1,5 +1,4 @@
import { createConfirmation } from 'react-confirm';
import { toast } from 'react-toastify';
import ConfirmModal from '../components/includes/ConfirmModal';
import api from '../api';
@ -76,7 +75,6 @@ const utils = {
return confirm({ title, confirmation: content });
},
rgb(s) {
if (!s) return s;
if (s.match(/^[0-9]+,[0-9]+,[0-9]+$/)) return `rgb(${s})`;
if (s.match(/^rgb\([0-9]+,[0-9]+[0-9]+\)$/)) return s;
if (s.match(/^#[a-zA-Z0-9]+$/)) {
@ -120,61 +118,7 @@ const utils = {
});
}
});
},
downloadDrawdownImage(object) {
const element = document.createElement('a');
element.setAttribute('target', '_blank');
element.setAttribute('href', object.previewUrl);
element.setAttribute('download', `${object.name.replace(/ /g, '_')}-drawdown.png`);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
toast.info('The file has been downloaded');
},
downloadPatternImage(object) {
const element = document.createElement('a');
element.setAttribute('target', '_blank');
element.setAttribute('href', object.patternImage);
element.setAttribute('download', `${object.name.replace(/ /g, '_')}-pattern.png`);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
toast.info('The file has been downloaded');
},
generateCompletePattern(pattern, parentSelector, cb) {
if (!pattern) return;
const { warp, weft } = pattern;
setTimeout(() => {
const baseSize = 6;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = warp.threading?.length * baseSize + weft.treadles * baseSize + 20;
canvas.height = warp.shafts * baseSize + weft.treadling?.length * baseSize + 20;
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'black';
const warpCanvas = document.querySelector(`${parentSelector} .warp-threads`);
const warpColourwayCanvas = document.querySelector(`${parentSelector} .warp-colourway`);
const weftCanvas = document.querySelector(`${parentSelector} .weft-threads`);
const weftColourwayCanvas = document.querySelector(`${parentSelector} .weft-colourway`);
const drawdownCanvas = document.querySelector(`${parentSelector} .drawdown`);
const tieupsCanvas = document.querySelector(`${parentSelector} .tieups`);
if (warpCanvas) {
ctx.drawImage(warpColourwayCanvas, canvas.width - warpCanvas.width - weft.treadles * baseSize - 20, 0);
ctx.drawImage(warpCanvas, canvas.width - warpCanvas.width - weft.treadles * baseSize - 20, 10);
ctx.drawImage(weftCanvas, canvas.width - 10 - weft.treadles * baseSize, warp.shafts * baseSize + 20);
ctx.drawImage(weftColourwayCanvas, canvas.width - 10, warp.shafts * baseSize + 20);
ctx.drawImage(tieupsCanvas, canvas.width - weft.treadles * baseSize - 10, 10);
ctx.drawImage(drawdownCanvas, canvas.width - drawdownCanvas.width - weft.treadles * baseSize - 20, warp.shafts * baseSize + 20);
setTimeout(() => {
const im = canvas.toDataURL('image/png')
if (im?.length > 20) cb(im);
}, 500);
}
}, 500);
},
}
};
export default utils;

View File

@ -424,7 +424,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/runtime@npm:^7.12.1":
"@babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.7.6":
version: 7.17.9
resolution: "@babel/runtime@npm:7.17.9"
dependencies:
@ -691,13 +691,6 @@ __metadata:
languageName: node
linkType: hard
"@remix-run/router@npm:1.14.1":
version: 1.14.1
resolution: "@remix-run/router@npm:1.14.1"
checksum: a3a0e7bd1917a4fd10322269b78ce1c5917a74889dfa747305b5a56e5d441b9187e1d8aaa6d58c4bf77713e7baca825f87057a64b2d21718cdb361f1cda75687
languageName: node
linkType: hard
"@rollup/pluginutils@npm:^4.2.1":
version: 4.2.1
resolution: "@rollup/pluginutils@npm:4.2.1"
@ -1991,6 +1984,15 @@ __metadata:
languageName: node
linkType: hard
"history@npm:^5.2.0":
version: 5.3.0
resolution: "history@npm:5.3.0"
dependencies:
"@babel/runtime": ^7.7.6
checksum: d73c35df49d19ac172f9547d30a21a26793e83f16a78386d99583b5bf1429cc980799fcf1827eb215d31816a6600684fba9686ce78104e23bd89ec239e7c726f
languageName: node
linkType: hard
"hoist-non-react-statics@npm:^3.0.0, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2":
version: 3.3.2
resolution: "hoist-non-react-statics@npm:3.3.2"
@ -3055,27 +3057,27 @@ __metadata:
languageName: node
linkType: hard
"react-router-dom@npm:^6.21.1":
version: 6.21.1
resolution: "react-router-dom@npm:6.21.1"
"react-router-dom@npm:^6.3.0":
version: 6.3.0
resolution: "react-router-dom@npm:6.3.0"
dependencies:
"@remix-run/router": 1.14.1
react-router: 6.21.1
history: ^5.2.0
react-router: 6.3.0
peerDependencies:
react: ">=16.8"
react-dom: ">=16.8"
checksum: d8ea3370babab626827008ced8a776ac991023745dc69d44332b6a5229dd2a6899f3e46fc50365cef0c7487cf726bcdccb365ec106915f92c735da6e4c2e6b97
checksum: 77603a654f8a8dc7f65535a2e5917a65f8d9ffcb06546d28dd297e52adcc4b8a84377e0115f48dca330b080af2da3e78f29d590c89307094d36927d2b1751ec3
languageName: node
linkType: hard
"react-router@npm:6.21.1":
version: 6.21.1
resolution: "react-router@npm:6.21.1"
"react-router@npm:6.3.0":
version: 6.3.0
resolution: "react-router@npm:6.3.0"
dependencies:
"@remix-run/router": 1.14.1
history: ^5.2.0
peerDependencies:
react: ">=16.8"
checksum: c6774cf4440b524401cecb40f4fbf4dbc89d61405f9f6d3cb3d2338e262a5254b9e27bc6039dc32e2c5f8b3009e83e9d6b30a61159dd6c423297cb0e3bd46fb3
checksum: 7be673f5e72104be01e6ab274516bdb932efd93305243170690f6560e3bd1035dd1df3d3c9ce1e0f452638a2529f43a1e77dcf0934fc8031c4783da657be13ca
languageName: node
linkType: hard
@ -3577,7 +3579,7 @@ __metadata:
react-joyride: ^2.4.0
react-redux: ^8.0.1
react-rewards: ^2.0.3
react-router-dom: ^6.21.1
react-router-dom: ^6.3.0
react-toastify: ^4.4.0
redux: ^4.0.0
sanitize-html: ^1.19.1