Skip to content

Commit

Permalink
⚡ Add dnd-processor tag that solves edge cases with drag-and-drop beh…
Browse files Browse the repository at this point in the history
…avior and allows dropping any supported files on any tab
  • Loading branch information
CosmoMyzrailGorynych committed Aug 24, 2020
1 parent 7f04e30 commit f00de1a
Show file tree
Hide file tree
Showing 10 changed files with 362 additions and 303 deletions.
66 changes: 33 additions & 33 deletions src/js/loadProject.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,6 @@
lastProjects.pop();
}
localStorage.lastProjects = lastProjects.join(';');
window.signals.trigger('hideProjectSelector');
window.signals.trigger('projectLoaded');

if (global.currentProject.settings.title) {
document.title = global.currentProject.settings.title + ' — ct.js';
Expand All @@ -138,6 +136,7 @@
resetTypedefs();
loadAllTypedefs();

window.signals.trigger('projectLoaded');
setTimeout(() => {
window.riot.update();
}, 0);
Expand Down Expand Up @@ -187,7 +186,7 @@
}
};

window.loadProject = proj => {
window.loadProject = async proj => {
if (!proj) {
const baseMessage = 'An attempt to open a project with an empty path.';
alertify.error(baseMessage + ' See the console for the call stack.');
Expand All @@ -197,36 +196,37 @@
sessionStorage.projname = path.basename(proj);
global.projdir = path.dirname(proj) + path.sep + path.basename(proj, '.ict');

fs.stat(proj + '.recovery', (err, stat) => {
if (!err && stat.isFile()) {
var targetStat = fs.statSync(proj),
voc = window.languageJSON.intro.recovery;
window.alertify
.okBtn(voc.loadRecovery)
.cancelBtn(voc.loadTarget)
/* {0} — target file date
{1} — target file state (newer/older)
{2} — recovery file date
{3} — recovery file state (newer/older)
*/
.confirm(voc.message
.replace('{0}', targetStat.mtime.toLocaleString())
.replace('{1}', targetStat.mtime < stat.mtime ? voc.older : voc.newer)
.replace('{2}', stat.mtime.toLocaleString())
.replace('{3}', stat.mtime < targetStat.mtime ? voc.older : voc.newer))
.then(e => {
if (e.buttonClicked === 'ok') {
loadProjectFile(proj + '.recovery');
} else {
loadProjectFile(proj);
}
window.alertify
.okBtn(window.languageJSON.common.ok)
.cancelBtn(window.languageJSON.common.cancel);
});
} else {
loadProjectFile(proj);
let recoveryStat;
try {
recoveryStat = await fs.stat(proj + '.recovery');
} catch (err) {
// no recovery file found
void 0;
}
if (recoveryStat && recoveryStat.isFile()) {
const targetStat = await fs.stat(proj);
const voc = window.languageJSON.intro.recovery;
const userResponse = await window.alertify
.okBtn(voc.loadRecovery)
.cancelBtn(voc.loadTarget)
/* {0} — target file date
{1} — target file state (newer/older)
{2} — recovery file date
{3} — recovery file state (newer/older)
*/
.confirm(voc.message
.replace('{0}', targetStat.mtime.toLocaleString())
.replace('{1}', targetStat.mtime < recoveryStat.mtime ? voc.older : voc.newer)
.replace('{2}', recoveryStat.mtime.toLocaleString())
.replace('{3}', recoveryStat.mtime < targetStat.mtime ? voc.older : voc.newer));
window.alertify
.okBtn(window.languageJSON.common.ok)
.cancelBtn(window.languageJSON.common.cancel);
if (userResponse.buttonClicked === 'ok') {
return loadProjectFile(proj + '.recovery');
}
});
return loadProjectFile(proj);
}
return loadProjectFile(proj);
};
})(this);
16 changes: 16 additions & 0 deletions src/js/preventDnDNavigation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* This file prevents opening an image or such when a file was dragged into
* ct.js window and it was not catched by other listeners
*/

{
const draghHandler = function draghHandler(e) {
if (e.target.nodeName === 'INPUT' && e.target.type === 'file') {
return;
}
e.preventDefault();
};
document.addEventListener('dragenter', draghHandler);
document.addEventListener('dragover', draghHandler);
document.addEventListener('drop', draghHandler);
}
95 changes: 95 additions & 0 deletions src/node_requires/resources/fonts/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* @param {object|string} font The font object in ct.js project, or its UID.
* @param {boolean} fs If set to `true`, returns a clean path in a file system.
* Otherwise, returns an URL.
*/
const getPathToTtf = function getPathToTtf(font, fs) {
const path = require('path');
if (fs) {
return path.join(global.projdir, 'fonts', font.origname);
}
return `file://${global.projdir}/fonts/${font.origname}`;
};

/**
* @param {object|string} font The font object in ct.js project, or its UID.
* @param {boolean} fs If set to `true`, returns a clean path in a file system.
* Otherwise, returns an URL.
*/
const getFontPreview = function getFontPreview(font, fs) {
const path = require('path');
if (fs) {
return path.join(global.projdir, 'fonts', `${font.origname}_prev.png`);
}
return `file://${global.projdir}/fonts/${font.origname}_prev.png?cache=${font.lastmod}`;
};

const fontGenPreview = async function fontGenPreview(font) {
const template = {
weight: font.weight,
style: font.italic ? 'italic' : 'normal'
};
const fs = require('fs-extra');
const face = new FontFace('CTPROJFONT' + font.typefaceName, `url(${getPathToTtf(font)})`, template);

// Trigger font loading by creating an invisible label with this font
// const elt = document.createElement('span');
// elt.innerHTML = 'testString';
// elt.style.position = 'fixed';
// elt.style.right = '200%';
// elt.style.fontFamily = 'CTPROJFONT' + font.typefaceName;
// document.body.appendChild(elt);

const loaded = await face.load();
loaded.external = true;
loaded.ctId = face.ctId = font.uid;
document.fonts.add(loaded);
// document.body.removeChild(elt);

const c = document.createElement('canvas');
c.x = c.getContext('2d');
c.width = c.height = 64;
c.x.clearRect(0, 0, 64, 64);
c.x.font = `${font.italic ? 'italic ' : ''}${font.weight} ${Math.floor(64 * 0.75)}px "${loaded.family}"`;
c.x.fillStyle = '#000';
c.x.fillText('Aa', 64 * 0.05, 64 * 0.75);

// strip off the data:image url prefix to get just the base64-encoded bytes
const dataURL = c.toDataURL();
const previewBuffer = dataURL.replace(/^data:image\/\w+;base64,/, '');
const buf = new Buffer(previewBuffer, 'base64');
await fs.writeFile(getFontPreview(font, true), buf);
};

const importTtfToFont = async function importTtfToFont(src) {
const fs = require('fs-extra'),
path = require('path');
if (path.extname(src) !== '.ttf') {
throw new Error(`[resources/fonts] Rejecting a file as it does not have a .ttf extension: ${src}`);
}
const generateGUID = require('./../../generateGUID');
const uid = generateGUID();
await fs.copy(src, path.join(global.projdir, '/fonts/f' + uid + '.ttf'));
const obj = {
typefaceName: path.basename(src).replace('.ttf', ''),
weight: 400,
italic: false,
origname: `f${uid}.ttf`,
lastmod: Number(new Date()),
pixelFont: false,
pixelFontSize: 16,
pixelFontLineHeight: 18,
charsets: ['allInFont'],
customCharset: '',
uid
};
global.currentProject.fonts.push(obj);
await fontGenPreview(obj);
window.signals.trigger('fontCreated');
};
module.exports = {
importTtfToFont,
fontGenPreview,
getFontPreview,
getPathToTtf
};
106 changes: 106 additions & 0 deletions src/node_requires/resources/skeletons.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
const path = require('path');

const getSkeletonData = function getSkeletonData(skeleton, fs) {
if (fs) {
return path.join(global.projdir, 'img', skeleton.origname);
}
return `file://${global.projdir}/img/${skeleton.origname}`;
};
const getSkeletonTextureData = function getSkeletonTextureData(skeleton, fs) {
const slice = skeleton.origname.replace('_ske.json', '');
if (fs) {
return path.join(global.projdir, 'img', `${slice}_tex.json`);
}
return `file://${global.projdir}/img/${slice}_tex.json`;
};
const getSkeletonTexture = function getSkeletonTexture(skeleton, fs) {
const slice = skeleton.origname.replace('_ske.json', '');
if (fs) {
return path.join(global.projdir, 'img', `${slice}_tex.png`);
}
return `file://${global.projdir}/img/${slice}_tex.png`;
};

const getSkeletonPreview = function getSkeletonPreview(skeleton, fs) {
if (fs) {
return path.join(global.projdir, 'img', `${skeleton.origname}_prev.png`);
}
return `file://${global.projdir}/img/${skeleton.origname}_prev.png`;
};

/**
* Generates a square thumbnail of a given skeleton
* @param {String} skeleton The skeleton object to generate a preview for.
* @returns {Promise<void>} Resolves after creating a thumbnail.
*/
const skeletonGenPreview = function (skeleton) {
const loader = new PIXI.loaders.Loader(),
dbf = dragonBones.PixiFactory.factory;
const fs = require('fs-extra');
return new Promise((resolve, reject) => {
// Draw the armature on a canvas/in a Pixi.js app
const skelData = getSkeletonData(skeleton),
texData = getSkeletonTextureData(skeleton),
tex = getSkeletonTexture(skeleton);
loader.add(skelData, skelData)
.add(texData, texData)
.add(tex, tex);
loader.load(() => {
dbf.parseDragonBonesData(loader.resources[skelData].data);
dbf.parseTextureAtlasData(
loader.resources[texData].data,
loader.resources[tex].texture
);
const skel = dbf.buildArmatureDisplay('Armature', loader.resources[skelData].data.name);

const app = new PIXI.Application();

const rawSkelBase64 = app.renderer.plugins.extract.base64(skel);
const skelBase64 = rawSkelBase64.replace(/^data:image\/\w+;base64,/, '');
const buf = new Buffer(skelBase64, 'base64');

fs.writeFile(getSkeletonPreview(skeleton, true), buf)
.then(() => {
// Clean memory from DragonBones' armatures
// eslint-disable-next-line no-underscore-dangle
delete dbf._dragonBonesDataMap[loader.resources[skelData].data.name];
// eslint-disable-next-line no-underscore-dangle
delete dbf._textureAtlasDataMap[loader.resources[skelData].data.name];
})
.then(resolve)
.catch(reject);
});
});
};

const importSkeleton = async function importSkeleton(source) {
const generateGUID = require('./../generateGUID');
const fs = require('fs-extra');

const uid = generateGUID();
const partialDest = path.join(global.projdir + '/img/skdb' + uid);

await Promise.all([
fs.copy(source, partialDest + '_ske.json'),
fs.copy(source.replace('_ske.json', '_tex.json'), partialDest + '_tex.json'),
fs.copy(source.replace('_ske.json', '_tex.png'), partialDest + '_tex.png')
]);
const skel = {
name: path.basename(source).replace('_ske.json', ''),
origname: path.basename(partialDest + '_ske.json'),
from: 'dragonbones',
uid
};
await skeletonGenPreview(skel);
global.currentProject.skeletons.push(skel);
window.signals.trigger('skeletonImported', skel);
};

module.exports = {
getSkeletonData,
getSkeletonTextureData,
getSkeletonTexture,
getSkeletonPreview,
skeletonGenPreview,
importSkeleton
};
68 changes: 68 additions & 0 deletions src/riotTags/dnd-processor.tag
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
dnd-processor
.aDropzone(if="{dropping}")
.middleinner
svg.feather
use(xlink:href="data/icons.svg#download")
h2 {languageJSON.common.fastimport}
input(
type="file" multiple
accept=".png,.jpg,.jpeg,.bmp,.gif,.json,.ttf"
onchange="{dndImport}"
)
script.
this.dndImport = e => {
const files = [...e.target.files].map(file => file.path);
for (let i = 0; i < files.length; i++) {
if (/\.(jpg|gif|png|jpeg)/gi.test(files[i])) {
const {importImageToTexture} = require('./data/node_requires/resources/textures');
importImageToTexture(files[i]);
} else if (/_ske\.json/i.test(files[i])) {
const {importSkeleton} = require('./data/node_requires/resources/skeletons');
importSkeleton(files[i]);
} else if (/\.ttf/gi.test(files[i])) {
const {importTtfToFont} = require('./data/node_requires/resources/fonts');
importTtfToFont(files[i]);
} else {
alertify.log(`Skipped ${files[i]} as it is not supported by drag-and-drop importer.`);
}
}
e.srcElement.value = '';
this.dropping = false;
e.preventDefault();
};

/*
* drag-n-drop handling
*/
let dragTimer;
this.onDragOver = e => {
var dt = e.dataTransfer;
if (dt.types && (dt.types.indexOf ? dt.types.indexOf('Files') !== -1 : dt.types.contains('Files'))) {
this.dropping = true;
this.update();
window.clearTimeout(dragTimer);
}
e.preventDefault();
e.stopPropagation();
};
this.onDrop = e => {
e.stopPropagation();
};
this.onDragLeave = e => {
dragTimer = window.setTimeout(() => {
this.dropping = false;
this.update();
}, 25);
e.preventDefault();
e.stopPropagation();
};
this.on('mount', () => {
document.addEventListener('dragover', this.onDragOver);
document.addEventListener('dragleave', this.onDragLeave);
document.addEventListener('drop', this.onDrop);
});
this.on('unmount', () => {
document.removeEventListener('dragover', this.onDragOver);
document.removeEventListener('dragleave', this.onDragLeave);
document.removeEventListener('drop', this.onDrop);
});
Loading

0 comments on commit f00de1a

Please sign in to comment.