Merge branch 'master' into master
This commit is contained in:
1
app/.gitignore
vendored
1
app/.gitignore
vendored
@@ -9,3 +9,4 @@ nohup.out
|
||||
node_modules
|
||||
dump
|
||||
.idea/
|
||||
.cache
|
||||
|
||||
@@ -31,9 +31,17 @@
|
||||
Settings
|
||||
</paper-icon-item>
|
||||
<paper-icon-item id="characterExport">
|
||||
<iron-icon icon="content-copy" item-icon></iron-icon>
|
||||
<iron-icon icon="exit-to-app" item-icon></iron-icon>
|
||||
Export to Improved Initiative
|
||||
</paper-icon-item>
|
||||
<paper-icon-item id="characterCopy">
|
||||
<iron-icon icon="content-copy" item-icon></iron-icon>
|
||||
Make a copy
|
||||
</paper-icon-item>
|
||||
<paper-icon-item id="characterDump">
|
||||
<iron-icon icon="file-download" item-icon></iron-icon>
|
||||
Download a backup
|
||||
</paper-icon-item>
|
||||
</paper-menu>
|
||||
</paper-menu-button>
|
||||
{{else}}
|
||||
|
||||
@@ -234,6 +234,18 @@ Template.characterSheet.events({
|
||||
element: event.currentTarget.parentElement.parentElement,
|
||||
});
|
||||
},
|
||||
"click #characterCopy": function(event, instance){
|
||||
Meteor.call("copyCharacter", this._id, (error, char) => {
|
||||
if (error){
|
||||
console.error(error);
|
||||
} else {
|
||||
Router.go(`/character/${char._id}/${char.urlName || "-"}`);
|
||||
}
|
||||
});
|
||||
},
|
||||
"click #characterDump": function(event, instance){
|
||||
saveCharacterDump(this._id);
|
||||
},
|
||||
"click #unshareCharacter": function(event, instance){
|
||||
pushDialogStack({
|
||||
data: this,
|
||||
|
||||
@@ -58,6 +58,13 @@
|
||||
</paper-fab>
|
||||
{{#simpleTooltip class="always"}} New Character {{/simpleTooltip}}
|
||||
</div>
|
||||
<div>
|
||||
<paper-fab icon="file-upload"
|
||||
class="restoreCharacter"
|
||||
mini>
|
||||
</paper-fab>
|
||||
{{#simpleTooltip class="always"}} Restore from backup {{/simpleTooltip}}
|
||||
</div>
|
||||
{{/fabMenu}}
|
||||
</div>
|
||||
</app-header-layout>
|
||||
|
||||
@@ -81,4 +81,13 @@ Template.characterList.events({
|
||||
returnElement: instance.find(`.party[data-id='${partyId}']`),
|
||||
});
|
||||
},
|
||||
"click .restoreCharacter": function(event, instance) {
|
||||
pushDialogStack({
|
||||
template: "characterRestoreDialog",
|
||||
element: event.currentTarget,
|
||||
callback(dump){
|
||||
return;
|
||||
},
|
||||
})
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<template name="characterRestoreDialog">
|
||||
<div class="fit layout vertical">
|
||||
<app-header-layout has-scrolling-region class="new-character-dialog flex">
|
||||
<app-header fixed effects="waterfall">
|
||||
<app-toolbar>
|
||||
<div main-title>Restore Character</div>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
<div class="form">
|
||||
<p>
|
||||
Restore a character from a backup file, this will create a new copy of
|
||||
the restored character
|
||||
</p>
|
||||
<paper-input class="fileInput" type="file" label="File"></paper-input><br>
|
||||
{{#if error}}
|
||||
<p style="color: red;">
|
||||
{{error}}
|
||||
</p>
|
||||
{{/if}}
|
||||
{{#if loading}}
|
||||
<paper-spinner active></paper-spinner>
|
||||
{{/if}}
|
||||
</div>
|
||||
</app-header-layout>
|
||||
<div class="buttons layout horizontal end-justified">
|
||||
<paper-button class="cancelButton">
|
||||
Cancel
|
||||
</paper-button>
|
||||
<paper-button class="addButton" disabled={{invalid}}>
|
||||
Restore
|
||||
</paper-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,66 @@
|
||||
Template.characterRestoreDialog.onCreated(function(){
|
||||
this.dump = {};
|
||||
this.valid = new ReactiveVar(false);
|
||||
this.error = new ReactiveVar(null);
|
||||
this.loading = new ReactiveVar(false);
|
||||
});
|
||||
|
||||
Template.characterRestoreDialog.helpers({
|
||||
invalid(){
|
||||
return !Template.instance().valid.get();
|
||||
},
|
||||
error(){
|
||||
return Template.instance().error.get();
|
||||
},
|
||||
loading(){
|
||||
return Template.instance().loading.get();
|
||||
},
|
||||
});
|
||||
|
||||
const fail = function(instance){
|
||||
instance.valid.set(false);
|
||||
instance.error.set("Failed to convert file into a valid character");
|
||||
instance.dump = undefined;
|
||||
};
|
||||
|
||||
Template.characterRestoreDialog.events({
|
||||
"input .fileInput": function(event, instance){
|
||||
let input = event.currentTarget.$.input;
|
||||
let reader = new FileReader();
|
||||
reader.onload = function(){
|
||||
let dumpString = reader.result;
|
||||
try {
|
||||
let dump = JSON.parse(dumpString);
|
||||
if (dump && dump.character && dump.collections){
|
||||
instance.valid.set(true);
|
||||
instance.error.set(null);
|
||||
instance.dump = dump;
|
||||
} else {
|
||||
fail(instance);
|
||||
}
|
||||
} catch (e) {
|
||||
fail(instance);
|
||||
}
|
||||
};
|
||||
reader.readAsText(input.files[0]);
|
||||
},
|
||||
"click .cancelButton": function(event, instance){
|
||||
popDialogStack();
|
||||
},
|
||||
"click .addButton": function(event, instance){
|
||||
let dump = instance.dump;
|
||||
if (!dump) return;
|
||||
Meteor.call('restoreCharacter', dump, (e, char) => {
|
||||
instance.loading.set(false);
|
||||
if (!char){
|
||||
instance.error.set(e.message)
|
||||
} else {
|
||||
popDialogStack();
|
||||
Router.go("characterSheet", {
|
||||
_id: char._id,
|
||||
urlName: char.urlName || '-',
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
99
app/lib/functions/backupRestoreCharacter.js
Normal file
99
app/lib/functions/backupRestoreCharacter.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
let characterCollections = [];
|
||||
Meteor.startup(() => {
|
||||
characterCollections = [
|
||||
Actions,
|
||||
Attacks,
|
||||
Buffs,
|
||||
Classes,
|
||||
Conditions,
|
||||
CustomBuffs,
|
||||
Effects,
|
||||
Experiences,
|
||||
Features,
|
||||
Notes,
|
||||
Proficiencies,
|
||||
SpellLists,
|
||||
Spells,
|
||||
TemporaryHitPoints,
|
||||
Items,
|
||||
Containers,
|
||||
];
|
||||
});
|
||||
|
||||
dumpCharacter = function(charId){
|
||||
let characterDump = {collections: {}};
|
||||
characterDump.character = Characters.findOne(charId);
|
||||
characterCollections.forEach(c => {
|
||||
characterDump.collections[c._name] = c.find({charId}).fetch();
|
||||
});
|
||||
return characterDump;
|
||||
};
|
||||
|
||||
saveCharacterDump = function(charId){
|
||||
let dump = dumpCharacter(charId);
|
||||
let textDump = JSON.stringify(dump, null, 2);
|
||||
let charName = dump.character.name;
|
||||
let blob = new Blob([textDump], {type: "application/json;charset=utf-8"});
|
||||
saveAs(blob, `${charName}.JSON`);
|
||||
};
|
||||
|
||||
giveCharacterDumpNewIds = function(characterDump){
|
||||
// Give the character a new Id
|
||||
const oldCharId = characterDump.character._id;
|
||||
const newCharId = Random.id();
|
||||
characterDump.character._id = newCharId;
|
||||
|
||||
let idMap = {[oldCharId]: newCharId}; // {oldId: newId}
|
||||
|
||||
// Give all documents a new Id, and store the mapping from old to new
|
||||
for (let colName in characterDump.collections){
|
||||
for (let doc of characterDump.collections[colName]){
|
||||
let oldId = doc._id;
|
||||
let newId = Random.id();
|
||||
doc._id = newId;
|
||||
idMap[oldId] = newId;
|
||||
}
|
||||
}
|
||||
|
||||
// Replace all references to old Ids with new ones
|
||||
for (let colName in characterDump.collections){
|
||||
for (let doc of characterDump.collections[colName]){
|
||||
// Replace the character Id with the new one
|
||||
doc.charId = newCharId;
|
||||
// Replace the parent reference id with a new id
|
||||
if (doc.parent && doc.parent.id){
|
||||
let newParentId = idMap[doc.parent.id];
|
||||
if(!newParentId) throw `Can't find the mapping for id ${doc.parent.id}`;
|
||||
doc.parent.id = newParentId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
restoreCharacter = function(characterDump){
|
||||
Characters.direct.insert(characterDump.character);
|
||||
for (collectionName in characterDump.collections){
|
||||
let collection = Meteor.Collection.get(collectionName);
|
||||
for (doc of characterDump.collections[collectionName]){
|
||||
// delete problematic keys that shouldn't ever be available on insert
|
||||
delete doc.restoredAt;
|
||||
delete doc.restoredBy;
|
||||
// Insert the doc with no hooks
|
||||
collection.direct.insert(doc);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Meteor.methods({
|
||||
restoreCharacter(characterDump){
|
||||
characterDump.character.name += " - Restored"
|
||||
characterDump.character.owner = Meteor.userId();
|
||||
characterDump.character.readers = [];
|
||||
characterDump.character.writers = [];
|
||||
giveCharacterDumpNewIds(characterDump);
|
||||
restoreCharacter(characterDump);
|
||||
return characterDump.character
|
||||
},
|
||||
});
|
||||
@@ -12,12 +12,14 @@ canEditCharacter = function(charId, userId){
|
||||
return (userId === char.owner || _.contains(char.writers, userId));
|
||||
};
|
||||
|
||||
canViewCharacter = function(charId, userId){
|
||||
canViewCharacter = function(char, userId){
|
||||
userId = userId || Meteor.userId();
|
||||
var char = Characters.findOne(
|
||||
charId,
|
||||
{fields: {owner: 1, writers: 1, readers: 1, "settings.viewPermission": 1}}
|
||||
);
|
||||
if (typeof char !== 'object'){
|
||||
char = Characters.findOne(
|
||||
char,
|
||||
{fields: {owner: 1, writers: 1, readers: 1, "settings.viewPermission": 1}}
|
||||
);
|
||||
}
|
||||
if (!char) return true;
|
||||
return userId === char.owner ||
|
||||
char.settings.viewPermission === "public" ||
|
||||
|
||||
26
app/lib/methods/characterCopyPaste.js
Normal file
26
app/lib/methods/characterCopyPaste.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// Uses '/lib/functions/backupRestoreCharacter.js' to do most the work
|
||||
|
||||
Meteor.methods({
|
||||
copyCharacter: function(charId) {
|
||||
const userId = Meteor.userId();
|
||||
let character = Characters.findOne(charId);
|
||||
|
||||
// Need at least view level permission to make a copy for yourself
|
||||
if (!canViewCharacter(character, userId)) return;
|
||||
|
||||
let characterDump = dumpCharacter(charId);
|
||||
giveCharacterDumpNewIds(characterDump);
|
||||
|
||||
// Remove all readers and writers, make this user the new owner
|
||||
characterDump.character.readers = [];
|
||||
characterDump.character.writers = [];
|
||||
characterDump.character.owner = userId;
|
||||
|
||||
// Rename the character so it's obviously a copy
|
||||
characterDump.character.name += " - Copy";
|
||||
|
||||
// Write the character back to the database
|
||||
restoreCharacter(characterDump);
|
||||
return characterDump.character;
|
||||
},
|
||||
});
|
||||
93
app/package-lock.json
generated
93
app/package-lock.json
generated
@@ -45,7 +45,7 @@
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
|
||||
},
|
||||
"aproba": {
|
||||
@@ -74,6 +74,7 @@
|
||||
},
|
||||
"block-stream": {
|
||||
"version": "0.0.9",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=",
|
||||
"requires": {
|
||||
"inherits": "~2.0.0"
|
||||
@@ -131,7 +132,7 @@
|
||||
},
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
|
||||
},
|
||||
"core-js": {
|
||||
@@ -139,6 +140,11 @@
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz",
|
||||
"integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw=="
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
|
||||
@@ -209,6 +215,11 @@
|
||||
"resolved": "https://registry.npmjs.org/fibers/-/fibers-2.0.2.tgz",
|
||||
"integrity": "sha512-HfVRxhYG7C8Jl9FqtrlElMR2z/8YiLQVDKf67MLY25Ic+ILx3ecmklfT1v3u+7P5/4vEFjuxaAFXhr2/Afwk5g=="
|
||||
},
|
||||
"file-saver": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.0.tgz",
|
||||
"integrity": "sha512-cYM1ic5DAkg25pHKgi5f10ziAM7RJU37gaH1XQlyNDrtUnzhC/dfoV9zf2OmF0RMKi42jG5B0JWBnPQqyj/G6g=="
|
||||
},
|
||||
"find-up": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
|
||||
@@ -259,7 +270,7 @@
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
|
||||
},
|
||||
"har-schema": {
|
||||
@@ -361,6 +372,7 @@
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||
},
|
||||
"invert-kv": {
|
||||
@@ -383,7 +395,7 @@
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
@@ -1064,6 +1076,37 @@
|
||||
"requires": {
|
||||
"inherits": "~2.0.1",
|
||||
"readable-stream": "^2.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"readable-stream": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||
}
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"stream-http": {
|
||||
@@ -1076,6 +1119,37 @@
|
||||
"readable-stream": "^2.3.3",
|
||||
"to-arraybuffer": "^1.0.0",
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"readable-stream": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||
}
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
@@ -1434,7 +1508,7 @@
|
||||
},
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
|
||||
},
|
||||
"os-homedir": {
|
||||
@@ -1578,6 +1652,7 @@
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.0",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
|
||||
},
|
||||
"strip-json-comments": {
|
||||
@@ -1815,7 +1890,7 @@
|
||||
},
|
||||
"set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
|
||||
},
|
||||
"shebang-command": {
|
||||
@@ -1833,7 +1908,7 @@
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
|
||||
},
|
||||
"source-map": {
|
||||
@@ -1909,7 +1984,7 @@
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
"resolved": false,
|
||||
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
@@ -2159,7 +2234,7 @@
|
||||
},
|
||||
"wrap-ansi": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
|
||||
"integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
|
||||
"requires": {
|
||||
"string-width": "^1.0.1",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"bower": "^1.7.9",
|
||||
"core-js": "^2.5.7",
|
||||
"fibers": "^2.0.2",
|
||||
"file-saver": "^2.0.0",
|
||||
"meteor-node-stubs": "^0.3.3",
|
||||
"qrcode": "^1.3.0",
|
||||
"source-map-support": "^0.5.9",
|
||||
|
||||
Reference in New Issue
Block a user