Compare commits

...

14 Commits
0.2.4 ... 0.2.7

Author SHA1 Message Date
Stefan Zermatten
2404845d51 Styling and rounding fixes for detail boxes 2015-05-12 11:34:56 +02:00
Stefan Zermatten
bf032bcdf3 Now only show edit and add buttons to writers, not readers 2015-05-12 11:34:37 +02:00
Stefan Zermatten
ff8ae89722 Improved feedback form style 2015-05-12 10:36:15 +02:00
Stefan Zermatten
80ca7307ce Added change log 2015-05-12 10:10:15 +02:00
Stefan Zermatten
a539b0bc6c limited how much info gets published to users about themselves 2015-05-12 09:33:32 +02:00
Stefan Zermatten
c6b3cad9c8 Improved character side list style 2015-05-12 09:32:40 +02:00
Stefan Zermatten
95b7b66390 Added quick feedback form 2015-05-12 09:32:28 +02:00
Stefan Zermatten
43c4122fe3 Fixed stack dragging within the same container 2015-05-11 17:10:58 +02:00
Stefan Zermatten
3f4dcc146a fixed charId's being out of date after re-parenting 2015-05-11 17:10:28 +02:00
Stefan Zermatten
e4600decd0 Added more things to except list for need sign in 2015-05-11 16:51:17 +02:00
Stefan Zermatten
f6df716870 Rewrote how item drag and drop works. Need to update charId's to keep up. 2015-05-11 16:51:02 +02:00
Stefan Zermatten
b99da301cd Updated meteor 2015-05-11 12:16:38 +02:00
Stefan Zermatten
0a01885300 Finished implementing useraccounts 2015-05-11 12:15:00 +02:00
Stefan Zermatten
5cb1515235 Began implementing useraccounts and permissions properly 2015-05-08 12:59:38 +02:00
43 changed files with 776 additions and 268 deletions

View File

@@ -20,3 +20,7 @@ mike:mocha
dburles:mongo-collection-instances
percolate:migrations
ecwyne:mathjs
useraccounts:polymer
accounts-google
splendido:accounts-meld
email

View File

@@ -1,9 +1,11 @@
accounts-base@1.2.0
accounts-google@1.0.4
accounts-oauth@1.1.5
accounts-password@1.1.1
accounts-ui@1.1.5
accounts-ui-unstyled@1.1.7
aldeed:collection2@2.3.3
aldeed:simple-schema@1.3.2
aldeed:simple-schema@1.3.3
amplify@1.0.0
autoupdate@1.2.1
base64@1.0.3
@@ -24,6 +26,7 @@ ejson@1.0.6
email@1.0.6
fastclick@1.0.3
geojson-utils@1.0.3
google@1.1.5
html-tools@1.0.4
htmljs@1.0.4
http@1.1.0
@@ -44,20 +47,22 @@ less@1.0.14
livedata@1.0.13
localstorage@1.0.3
logging@1.0.7
matb33:collection-hooks@0.7.11
matb33:collection-hooks@0.7.13
meteor@1.1.6
meteor-platform@1.2.2
mike:mocha@0.5.3
mike:mocha@0.5.4
minifiers@1.1.5
minimongo@1.0.8
mobile-status-bar@1.0.3
momentjs:moment@2.10.3
mongo@1.1.0
npm-bcrypt@0.7.8_2
oauth@1.1.4
oauth2@1.1.3
observe-sequence@1.0.6
ordered-dict@1.0.3
package-version-parser@3.0.3
percolate:migrations@0.7.3
percolate:migrations@0.7.5
practicalmeteor:chai@1.9.2_3
practicalmeteor:loglevel@1.1.0_3
random@1.0.3
@@ -72,16 +77,21 @@ sanjo:meteor-version@1.0.0
service-configuration@1.0.4
session@1.1.0
sha@1.0.3
softwarerero:accounts-t9n@1.0.9
spacebars@1.0.6
spacebars-compiler@1.0.6
splendido:accounts-emails-field@1.2.0
splendido:accounts-meld@1.3.0
srp@1.0.3
templating@1.1.1
tracker@1.0.7
ui@1.0.6
underscore@1.0.3
url@1.0.4
useraccounts:core@1.9.1
useraccounts:polymer@1.9.1
velocity:chokidar@0.12.6_1
velocity:core@0.6.0
velocity:core@0.6.1
velocity:html-reporter@0.5.3
velocity:meteor-internals@1.1.0_7
velocity:shim@0.1.0

View File

@@ -61,7 +61,6 @@ Effects.attachSchema(Schemas.Effect);
if (Meteor.isServer) Characters.after.insert(function(userId, char) {
Effects.insert({
charId: char._id,
type: "inate",
name: "Constitution modifier for each level",
stat: "hitPoints",
operation: "add",
@@ -69,11 +68,11 @@ if (Meteor.isServer) Characters.after.insert(function(userId, char) {
parent: {
id: char._id,
collection: "Characters",
group: "Inate",
},
});
Effects.insert({
charId: char._id,
type: "inate",
name: "Proficiency bonus by level",
stat: "proficiencyBonus",
operation: "add",
@@ -81,11 +80,11 @@ if (Meteor.isServer) Characters.after.insert(function(userId, char) {
parent: {
id: char._id,
collection: "Characters",
group: "Inate",
},
});
Effects.insert({
charId: char._id,
type: "inate",
name: "Dexterity Armor Bonus",
stat: "armor",
operation: "add",
@@ -93,11 +92,11 @@ if (Meteor.isServer) Characters.after.insert(function(userId, char) {
parent: {
id: char._id,
collection: "Characters",
group: "Inate",
},
});
Effects.insert({
charId: char._id,
type: "inate",
name: "Natural Armor",
stat: "armor",
operation: "base",
@@ -105,6 +104,7 @@ if (Meteor.isServer) Characters.after.insert(function(userId, char) {
parent: {
id: char._id,
collection: "Characters",
group: "Inate",
},
});
});

View File

@@ -19,6 +19,156 @@ Schemas.Item = new SimpleSchema({
Items.attachSchema(Schemas.Item);
var checkMovePermission = function(itemId, parent) {
var item = Items.findOne(itemId);
if (!item)
throw new Meteor.Error("No such item",
"An item could not be found to move");
//handle permissions
var permission = Meteor.call("canWriteCharacter", item.charId);
if (!permission){
throw new Meteor.Error("Access denied",
"Not permitted to move items from this character");
}
if (parent.collection === "Characters"){
permission = Meteor.call("canWriteCharacter", parent.id);
if (!permission){
throw new Meteor.Error("Access denied",
"Not permitted to move items to this character");
}
} else {
var parentCollectionObject = global[parent.collection];
var parentObject = null;
if (parentCollectionObject)
parentObject = parentCollectionObject.findOne(
parent.id, {fields: {_id: 1, charId: 1}}
);
if (!parentObject) throw new Meteor.Error(
"Invalid parent",
"The destination parent " + parent.id +
" does not exist in the collection " + parent.collection
);
if (parentObject.charId){
permission = Meteor.call("canWriteCharacter", parentObject.charId);
if (!permission){
throw new Meteor.Error("Access denied",
"Not permitted to move items to this character");
}
}
}
};
var moveItem = function(itemId, enable, parentCollection, parentId) {
var item = Items.findOne(itemId);
if (!item) return;
parentCollection = parentCollection || item.parent.collection;
parentId = parentId || item.parent.id;
if (Meteor.isServer) {
checkMovePermission(itemId, {collection: parentCollection, id: parentId});
}
//update the item provided the update will actually change something
if (
item.parent.collection !== parentCollection ||
item.parent.id !== parentId ||
item.enabled !== enable
){
Items.update(
itemId,
{$set: {
"parent.collection": parentCollection,
"parent.id": parentId,
enabled: enable,
}}
);
}
};
Meteor.methods({
moveItemToParent: function(itemId, parent) {
check(itemId, String);
check(parent, {collection: String, id: String});
moveItem(itemId, false, parent.collection, parent.id);
},
moveItemToCharacter: function(itemId, charId) {
check(itemId, String);
check(charId, String);
moveItem(itemId, false, "Characters", charId);
},
moveItemToContainer: function(itemId, containerId) {
check(itemId, String);
check(containerId, String);
moveItem(itemId, false, "Containers", containerId);
},
equipItem: function(itemId, charId){
check(itemId, String);
check(charId, String);
moveItem(itemId, true, "Characters", charId);
},
unequipItem: function(itemId, charId){
check(itemId, String);
check(charId, String);
moveItem(itemId, false, "Characters", charId);
},
splitItemToParent: function(itemId, moveQuantity, parent){
check(itemId, String);
check(moveQuantity, Number);
check(parent, {id: String, collection: String});
//get the item
var item = Items.findOne(itemId);
if (!item) return;
//don't bother moving nothing
if (moveQuantity <= 0 || item.quantity <= 0){
return;
}
//ensure we are only moving up to the current stack size
if (item.quantity < moveQuantity){
moveQuantity = this.quantity;
}
if (Meteor.isServer) {
checkMovePermission(itemId, parent);
}
//create a new item stack
var newStack = _.omit(EJSON.clone(item), "_id");
newStack.parent = parent;
newStack.quantity = moveQuantity;
//find out if we have an exact replica in the destination
var query = _.omit(newStack, ["parent", "quantity"]);
query["parent.collection"] = newStack.parent.collection;
query["parent.id"] = newStack.parent.id;
query._id = {$ne: itemId}; //make sure we don't join it to itself
var existingStack = Items.findOne(query);
if (existingStack){
//increase the existing stack's size
Items.update(
existingStack._id,
{$inc: {quantity: moveQuantity}}
);
} else {
//insert the new stack
Items.insert(newStack, function(err, id){
if (err) throw err;
//copy the children also
Meteor.call("cloneChildren", item._id, {collection: "Items", id: id});
});
}
//reduce the old stack's size
var oldQuantity = item.quantity - moveQuantity;
if (oldQuantity === 0){
Items.remove(itemId);
} else {
Items.update(itemId, {$set: {quantity: oldQuantity}});
}
},
});
Items.helpers({
totalValue: function(){
return this.value * this.quantity;
@@ -33,103 +183,6 @@ Items.helpers({
return this.name;
}
},
equip: function(characterId){
var charId = characterId || this.charId;
if (!charId || !Characters.findOne(charId)) throw "Invalid character";
if (this.parent.collection === "Characters" &&
this.parent.id === charId &&
this.enabled) {
return;
}
Items.update(
this._id,
{$set: {
"parent.collection": "Characters",
"parent.id": charId,
enabled: true,
}}
);
},
unequip: function(){
if (!this.enabled) return;
Items.update(this._id, {$set: {enabled: false}});
},
moveToContainer: function(containerId){
if (!containerId || !Containers.findOne(containerId)){
throw "Invalid container";
}
if (this.parent.collection === "Containers" &&
this.parent.id === containerId &&
!this.enabled) return;
Items.update(
this._id,
{$set: {
"parent.collection": "Containers",
"parent.id": containerId,
enabled: false,
}}
);
},
moveToCharacter: function(characterId){
if (!characterId || !Characters.findOne(characterId)) {
throw "Invalid character";
}
if (this.parent.collection === "Characters" &&
this.parent.id === characterId &&
!this.enabled) return;
Items.update(
this._id,
{$set: {
"parent.collection": "Characters",
"parent.id": characterId,
charId: characterId,
enabled: false,
}}
);
},
splitToParent: function(parent, moveQuantity){
check(parent, {id: String, collection: String});
check(moveQuantity, Number);
var parentCollection = Meteor.isClient ?
window[parent.collection] : global[parent.collection];
if (!parent.id || !parentCollection.findOne(parent.id)){
throw "Invalid parent";
}
var oldStack = this;
//we can only move as much as we have, leaving 0 behind at worst
if (oldStack.quantity < moveQuantity) moveQuantity = oldStack.quantity;
var oldQuantity = oldStack.quantity - moveQuantity;
var newStack = _.pick(oldStack, Schemas.Item.objectKeys());
newStack.parent = parent;
newStack.quantity = moveQuantity;
var existingStack = Items.findOne(_.omit(newStack, "quantity"));
var updateStackSize = function(){
if (oldQuantity > 0){
Items.update(oldStack._id, {$set: {quantity: oldQuantity}});
} else {
Items.remove(oldStack._id);
}
};
if (existingStack){
Items.update(
existingStack._id,
{$inc: {quantity: moveQuantity}},
{},
function(){
updateStackSize();
}
);
} else {
Items.insert(newStack, function(err, id){
if (err) throw err;
updateStackSize();
//copy the children also
Meteor.call("cloneChildren", oldStack._id, {collection: "Items", id: id});
});
}
},
});
Items.before.update(function(userId, doc, fieldNames, modifier, options){

View File

@@ -0,0 +1,27 @@
ChangeLogs = new Mongo.Collection("changeLogs");
Schemas.ChangeLog = new SimpleSchema({
version: {
type: String,
},
changes: {
type: [String],
},
});
ChangeLogs.attachSchema(Schemas.ChangeLog);
ChangeLogs.allow({
insert: function(userId, doc) {
var user = Meteor.users.findOne(userId);
if (user) return _.contains(user.roles, "admin");
},
update: function(userId, doc, fields, modifier) {
var user = Meteor.users.findOne(userId);
if (user) return _.contains(user.roles, "admin");
},
remove: function(userId, doc) {
var user = Meteor.users.findOne(userId);
if (user) return _.contains(user.roles, "admin");
},
});

View File

@@ -0,0 +1,60 @@
Reports = new Mongo.Collection("reports");
Schemas.Report = new SimpleSchema({
owner: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
title: {
type: String,
trim: false,
optional: true,
},
description: {
type: String,
trim: false,
optional: true,
},
type: {
type: String,
allowedValues: ["bug", "change", "feature", "general"],
defaultValue: "bug",
},
//the immediate impact of doing this action (eg. -1 rages)
severity: {
type: Number,
defaultValue: 5,
min: 1,
max: 10,
},
metaData: {
type: Object,
blackbox: true,
},
});
Reports.attachSchema(Schemas.Report);
Meteor.methods({
insertReport: function(report) {
check(report, {
title: String,
description: String,
type: String,
severity: Number,
metaData: Object,
});
report.owner = this.userId;
Reports.insert(report);
},
deleteReport: function(id) {
var user = Meteor.users.findOne(this.userId);
if (!_.contains(user.roles, "admin")){
throw new Meteor.Error(
"not admin",
"The user must be an administrator to delete feedback"
);
}
Reports.remove(id);
},
});

View File

@@ -1,44 +1,23 @@
Schema = {};
Schema.User = new SimpleSchema({
username: {
type: String,
regEx: /^[a-z0-9A-Z_]{3,15}$/,
optional: true,
},
emails: {
type: [Object],
// this must be optional if you also use other login services like facebook,
// but if you use only accounts-password, then it can be required
optional: true,
},
"emails.$.address": {
type: String,
regEx: SimpleSchema.RegEx.Email,
},
"emails.$.verified": {
type: Boolean
},
createdAt: {
type: Date
},
services: {
type: Object,
optional: true,
blackbox: true,
},
roles: {
type: [String],
optional: true,
},
});
Meteor.users.attachSchema(Schema.User);
Meteor.users.allow({
update: function(userId, doc, fields, modifier) {
return userId === doc._id &&
fields.length === 1 &&
fields[0] === "username";
if (
doc._id === userId &&
_.contains(fields, "username") &&
_.contains(fields, "profile") &&
fields.length === 2 &&
_.keys(modifier).length === 1 &&
modifier.$set &&
modifier.$set["profile.username"] &&
modifier.$set.username &&
_.keys(modifier.$set).length === 2
){
var expectedUsername = modifier.$set["profile.username"];
expectedUsername = expectedUsername.toLowerCase().replace(/\s+/gm, "");
if (modifier.$set.username !== expectedUsername){
return false;
}
var foundUser = Meteor.call("getUserId", expectedUsername);
return !foundUser || foundUser === userId;
}
}
});

View File

@@ -3,9 +3,29 @@ Router.configure({
layoutTemplate: "layout",
});
Router.plugin("ensureSignedIn", {
except: [
"home",
"atSignIn",
"atSignUp",
"atForgotPassword",
"atResetPwd",
"atEnrollAccount",
"atVerifyEmail",
"atresendVerificationEmail",
"loginButtons",
"notFound",
]
});
Router.plugin("dataNotFound", {notFoundTemplate: "notFound"});
Router.map(function() {
this.route("/", {
name: "home",
onAfterAction: function() {
document.title = appName;
},
});
this.route("characterList", {
@@ -33,7 +53,7 @@ Router.map(function() {
data: function() {
var data = Characters.findOne(
{_id: this.params._id},
{fields: {_id: 1, name: 1, color: 1}}
{fields: {_id: 1, name: 1, color: 1, writers: 1, readers: 1}}
);
return data;
},
@@ -56,4 +76,21 @@ Router.map(function() {
document.title = appName + " Account";
},
});
this.route("/changelog", {
name: "changeLog",
waitOn: function() {
return [
Meteor.subscribe("changeLog"),
]
},
data: {
changeLogs: function() {
return ChangeLogs.find({}, {sort: {version: -1}});
}
},
onAfterAction: function() {
document.title = appName;
},
});
});

View File

@@ -0,0 +1,6 @@
Template.registerHelper("canEditCharacter", function(charId) {
var char = Characters.findOne(charId)
var userId = Meteor.userId();
return char.owner === userId ||
_.contains(char.writers, userId);
});

View File

@@ -5,18 +5,24 @@
<div flex>
{{name}}
</div>
<div>
{{> colorDropdown}}
</div>
<paper-menu-button>
<paper-icon-button icon="more-vert" noink></paper-icon-button>
<paper-dropdown class="dropdown" halign="right">
<core-menu class="menu" style="color: black; color: rgba(0,0,0,0.87);">
<paper-item id="deleteCharacter"><core-icon icon="delete"></core-icon>Delete</paper-item>
<paper-item id="shareCharacter"><core-icon icon="social:share"></core-icon>Share</paper-item>
</core-menu>
</paper-dropdown>
</paper-menu-button>
{{#if canEditCharacter _id}}
<div>
{{> colorDropdown}}
</div>
<paper-menu-button>
<paper-icon-button icon="more-vert" noink></paper-icon-button>
<paper-dropdown class="dropdown" halign="right">
<core-menu class="menu" style="color: black; color: rgba(0,0,0,0.87);">
<paper-item id="deleteCharacter">
<core-icon icon="delete"></core-icon>Delete
</paper-item>
<paper-item id="shareCharacter">
<core-icon icon="social:share"></core-icon>Share
</paper-item>
</core-menu>
</paper-dropdown>
</paper-menu-button>
{{/if}}
<div class="bottom fit" horizontal layout>
<paper-tabs flex horizontal center layout id="characterSheetTabs" selected={{selectedTab}} class="{{colorClass}}">
<paper-tab name="stats">Stats</paper-tab>

View File

@@ -13,7 +13,7 @@ var getTab = function(charId){
Template.characterSheet.helpers({
selectedTab: function(){
return getTab(this._id);
}
},
});
Template.characterSheet.events({

View File

@@ -104,14 +104,16 @@
</div>
<div class="fab-buffer"></div>
</div>
<paper-fab id="addFeature"
class="floatyButton"
icon="add"
title="Add"
role="button"
tabindex="0"
aria-label="Add"
hero-id="main"></paper-fab>
{{#if canEditCharacter _id}}
<paper-fab id="addFeature"
class="floatyButton"
icon="add"
title="Add"
role="button"
tabindex="0"
aria-label="Add"
hero-id="main"></paper-fab>
{{/if}}
</div>
</template>

View File

@@ -77,10 +77,12 @@
</div>
<div class="fab-buffer"></div>
</div>
<paper-fab-menu id="inventoryAddMenu" icon="add" closeIcon="close" duration="0.3">
<paper-fab-menu-item id="addItem" icon="note-add" color="#d23f31" tooltip="Item"></paper-fab-menu-item>
<paper-fab-menu-item id="addContainer" icon="work" color="#d23f31" tooltip="Container"></paper-fab-menu-item>
</paper-fab-menu>
{{#if canEditCharacter _id}}
<paper-fab-menu id="inventoryAddMenu" icon="add" closeIcon="close" duration="0.3">
<paper-fab-menu-item id="addItem" icon="note-add" color="#d23f31" tooltip="Item"></paper-fab-menu-item>
<paper-fab-menu-item id="addContainer" icon="work" color="#d23f31" tooltip="Container"></paper-fab-menu-item>
</paper-fab-menu>
{{/if}}
</div>
</template>

View File

@@ -174,71 +174,93 @@ Template.inventoryItem.helpers({
Template.layout.events({
"dragstart .inventoryItem": function(event, instance){
event.originalEvent.dataTransfer.setData("dicecloud-id/items", this._id);
Session.set("inventory.dragItemId", this._id);
Session.set("inventory.dragItemOriginalContainer", this.container);
Session.set("inventory.dragItemOriginalCharacter", this.charId);
},
"dragover .itemContainer, dragenter .itemContainer":
function(event, instance){
if (_.contains(event.originalEvent.dataTransfer.types, "dicecloud-id/items")){
event.preventDefault();
}
},
"dragover .equipmentContainer, dragenter .equipmentContainer":
function(event, instance){
if (_.contains(event.originalEvent.dataTransfer.types, "dicecloud-id/items")){
event.preventDefault();
}
},
"dragover .carriedContainer, dragenter .carriedContainer":
function(event, instance){
if (_.contains(event.originalEvent.dataTransfer.types, "dicecloud-id/items")){
event.preventDefault();
}
},
"dragover .characterRepresentative, dragenter .characterRepresentative":
function(event, instance){
if (_.contains(event.originalEvent.dataTransfer.types, "dicecloud-id/items")){
event.preventDefault();
}
},
"dragend .inventoryItem": function(event, instance){
resetInvetorySession(); //this is a valid drop zone
Session.set("inventory.dragItemId", null);
},
"dragover .itemContainer": function(event, instance){
event.preventDefault();
},
"dragover .equipmentContainer": function(event, instance){
event.preventDefault();
},
"dragover .carriedContainer": function(event, instance){
event.preventDefault();
},
"drop .itemContainer": function(event, instacne){
var item = Items.findOne(Session.get("inventory.dragItemId"));
"drop .itemContainer": function(event, instance){
var itemId = event.originalEvent.dataTransfer.getData("dicecloud-id/items");
if (event.ctrlKey){
//split the stack to the container
GlobalUI.showDialog({
template: "splitStackDialog",
data: {
id: item._id,
id: itemId,
parentCollection: "Containers",
parentId: this._id,
},
});
} else {
//move item to the container
item.moveToContainer(this._id);
Meteor.call("moveItemToContainer", itemId, this._id);
}
resetInvetorySession();
Session.set("inventory.dragItemId", null);
},
"drop .equipmentContainer": function(event, instance){
var charId = Session.get("inventory.dragItemOriginalCharacter");
var item = Items.findOne(Session.get("inventory.dragItemId"));
item.equip(charId);
resetInvetorySession();
var itemId = event.originalEvent.dataTransfer.getData("dicecloud-id/items");
Meteor.call("equipItem", itemId, this._id);
Session.set("inventory.dragItemId", null);
},
"drop .carriedContainer": function(event, instance){
var charId = Session.get("inventory.dragItemOriginalCharacter");
var item = Items.findOne(Session.get("inventory.dragItemId"));
var itemId = event.originalEvent.dataTransfer.getData("dicecloud-id/items");
if (event.ctrlKey){
//split the stack to the container
GlobalUI.showDialog({
template: "splitStackDialog",
data: {
id: item._id,
id: itemId,
parentCollection: "Characters",
parentId: this._id,
},
});
} else {
//move item to the character
item.moveToCharacter(this._id);
Meteor.call("moveItemToCharacter", itemId, this._id);
}
resetInvetorySession();
Session.set("inventory.dragItemId", null);
},
"drop .characterRepresentative": function(event, instance) {
var itemId = event.originalEvent.dataTransfer.getData("dicecloud-id/items");
if (event.ctrlKey){
//split the stack to the container
GlobalUI.showDialog({
template: "splitStackDialog",
data: {
id: itemId,
parentCollection: "Characters",
parentId: this._id,
},
});
} else {
//move item to the character
Meteor.call("moveItemToCharacter", itemId, this._id);
}
Session.set("inventory.dragItemId", null);
},
});
var resetInvetorySession = function(){
_.defer(function(){
Session.set("inventory.dragItemId", null);
Session.set("inventory.dragItemOriginalContainer", null);
Session.set("inventory.dragItemOriginalCharacter", null);
});
};

View File

@@ -10,7 +10,7 @@
<template name="itemDetails">
<div layout horizontal wrap center justified class="headline">
{{#if weight}}<div class="sideMargin">{{totalWeight}}lbs</div>{{/if}}
{{#if weight}}<div class="sideMargin">{{round totalWeight}}lbs</div>{{/if}}
{{#if value}}<div>{{valueString totalValue}}</div>{{/if}}
</div>
<div layout horizontal wrap class="caption">

View File

@@ -81,13 +81,10 @@ Template.itemEdit.events({
},
"change #equippedInput": function(event){
var equipped = Template.instance().find("#equippedInput").checked;
var item = Items.findOne(this._id);
if (item){
if (equipped){
item.equip();
} else {
item.unequip();
}
if (equipped){
Meteor.call("equipItem", this._id, this.charId);
} else {
Meteor.call("unequipItem", this._id, this.charId);
}
},
"change #attunementCheckbox": function(event){
@@ -107,7 +104,6 @@ Template.containerDropdown.events({
var detail = event.originalEvent.detail;
if (!detail.isSelected) return;
var containerId = detail.item.getAttribute("name");
var item = Items.findOne(Template.currentData()._id);
item.moveToContainer(containerId);
Meteor.call("moveItemToContainer", Template.currentData()._id, containerId);
}
});

View File

@@ -7,13 +7,12 @@ Template.splitStackDialog.helpers({
Template.splitStackDialog.events({
"tap #moveButton": function(event, instance){
var item = Items.findOne(this.id);
if (item){
item.splitToParent(
{collection: this.parentCollection , id: this.parentId},
+instance.find("#quantityInput").value
);
}
Meteor.call(
"splitItemToParent",
this.id,
+instance.find("#quantityInput").value,
{collection: this.parentCollection , id: this.parentId}
);
},
"tap #oneButton":function(event, instance){
instance.find("#quantityInput").value = 1;

View File

@@ -1,7 +1,7 @@
<template name="experienceDialog">
{{#with experience}}
{{#baseDialog title=name class=colorClass hideColor="true" startEditing=../startEditing}}
<div horizontal layout center-justified>
<div horizontal layout center-justified class= "display2">
{{value}}
</div>
{{#if description}}

View File

@@ -68,6 +68,7 @@
<div class="fab-buffer"></div>
</div>
</div>
{{#if canEditCharacter _id}}
<paper-fab id="addNote"
class="floatyButton"
icon="add"
@@ -75,4 +76,5 @@
role="button"
tabindex="0"
hero-id="main"></paper-fab>
{{/if}}
</template>

View File

@@ -1,5 +1,8 @@
<template name="raceDialog">
{{#baseDialog title="Race" class=colorClass hideColor="true" hideDelete="true" startEditing=startEditing}}
<div horizontal layout center-justified class= "display2">
{{race}}
</div>
{{> effectsViewList charId=charId parentId=charId parentGroup="racial"}}
{{> proficiencyViewList charId=charId parentId=charId parentGroup="racial"}}
{{else}}

View File

@@ -91,8 +91,10 @@
<div class="fab-buffer"></div>
</div>
</div>
<paper-fab-menu id="inventoryAddMenu" icon="add" closeIcon="close" duration="0.3">
<paper-fab-menu-item id="addSpell" icon="note-add" color="#d23f31" tooltip="Spell"></paper-fab-menu-item>
<paper-fab-menu-item id="addSpellList" icon="work" color="#d23f31" tooltip="Spell List"></paper-fab-menu-item>
</paper-fab-menu>
{{#if canEditCharacter _id}}
<paper-fab-menu id="inventoryAddMenu" icon="add" closeIcon="close" duration="0.3">
<paper-fab-menu-item id="addSpell" icon="note-add" color="#d23f31" tooltip="Spell"></paper-fab-menu-item>
<paper-fab-menu-item id="addSpellList" icon="work" color="#d23f31" tooltip="Spell List"></paper-fab-menu-item>
</paper-fab-menu>
{{/if}}
</template>

View File

@@ -140,7 +140,12 @@ Template.attributeDialogView.helpers({
return char.attributeValue(this.statName);
},
sourceName: function(){
if (this.parent.collection === "Characters") return this.name;
if (this.parent.group === "racial"){
return this.getParent().race;
}
if (this.parent.collection === "Characters"){
return this.name;
}
return this.getParent().name;
},
operationName: function(){

View File

@@ -2,10 +2,14 @@
color: black;
color: rgba(0, 0, 0, 0.870588);
cursor: pointer;
font-size: 16px;
height: 40px;
overflow: hidden;
padding: 12px 0 12px 16px;
padding: 8px 0 8px 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
.singleLineItem core-icon {
height: 8px;
margin-right: 8px;
width: 8px;
}

View File

@@ -3,7 +3,11 @@
{{#if characters.count}}
<div>
{{#each characters}}
<div class="singleLineItem">{{name}}</div>
<div class="singleLineItem characterRepresentative"
layout horizontal center>
<core-icon icon="image:brightness-1"></core-icon>
<div>{{name}}</div>
</div>
{{/each}}
</div>
{{/if}}

View File

@@ -0,0 +1,30 @@
<template name="feedback">
<div class="feedback" style="min-width: 300px; min-height: 370px">
<div>
<paper-input id="feedbackTitle" label="Title" floatinglabel></paper-input>
</div>
<div>
<paper-dropdown-menu class="typeDropdown" label="Operation" flex>
<paper-dropdown layered class="dropdown">
<core-menu class="menu typeMenu" selected="general">
<paper-item name="general">General Feedback</paper-item>
<paper-item name="bug">Bug</paper-item>
<paper-item name="change">Suggested Change</paper-item>
<paper-item name="feature">Feature Request</paper-item>
</core-menu>
</paper-dropdown>
</paper-dropdown-menu>
</div>
<div layout horizontal center>
<div>Importance</div>
<paper-slider id="severity" max=10 min=1 value=5 snap></paper-slider>
</div>
<paper-input-decorator label="Description" floatinglabel layout vertical>
<paper-autogrow-textarea rows=10 maxRows=10>
<textarea id="feedbackDescription"></textarea>
</paper-autogrow-textarea>
</paper-input-decorator>
</div>
<paper-button id="cancelButton" affirmative>Cancel</paper-button>
<paper-button id="sendButton" affirmative>Send </paper-button>
</template>

View File

@@ -0,0 +1,14 @@
Template.feedback.events({
"tap #sendButton": function(event, instance) {
var report = {};
report.title = instance.find("#feedbackTitle").value;
report.severity = instance.find("#severity").value;
report.type = instance.find(".typeMenu").selected;
report.description = instance.find("#feedbackDescription").value;
report.metaData = {
url: window.location.href,
session: _.pairs(Session.keys),
};
Meteor.call("insertReport", report);
}
});

View File

@@ -1,18 +1,20 @@
<template name="layout">
<core-drawer-panel>
<core-header-panel drawer navigation flex mode="seamed" class="white">
<div id="accountSummary">
{{> loginButtons}}
{{#if currentUser}}
<div id="profileLink" style="text-decoration: underline; cursor: pointer;">
My account
{{profileLink}}
</div>
{{else}}
<a href="/sign-in" style="color: white;">Sign in</a>
{{/if}}
</div>
<div id="navPanel">
<core-item id="homeNav" icon="home" label="Home"></core-item>
{{> characterSideList}}
<core-item id="feedback" icon="bug-report" label="Send Feedback"></core-item>
<core-item id="changeLog" icon="list" label="Change Log"></core-item>
</div>
</core-header-panel>
<core-animated-pages main

View File

@@ -1,3 +1,7 @@
Template.layout.onCreated(function() {
this.subscribe("user");
});
Template.layout.rendered = function() {
$(window).on("popstate", GlobalUI.popStateHandler);
};
@@ -9,7 +13,11 @@ Template.layout.destroyed = function() {
Template.layout.helpers({
notSelected: function(){
return Session.get("global.ui.detailShow") ? "not-selected" : null;
}
},
profileLink: function() {
var user = Meteor.user();
return user.profile && user.profile.username || user.username || "My Account";
},
});
Template.layout.events({
@@ -19,4 +27,14 @@ Template.layout.events({
"tap #profileLink": function(event, instance){
Router.go("profile");
},
"tap #feedback": function(event, instance) {
GlobalUI.showDialog({
heading: "Feedback",
template: "feedback",
fullOnMobile: true,
});
},
"tap #changeLog": function(event, instance) {
Router.go("changeLog");
},
});

View File

@@ -0,0 +1,20 @@
<template name="changeLog">
<core-toolbar class="blue-grey white-text">
<core-icon-button icon="menu" core-drawer-toggle></core-icon-button>
<div flex>
Change Log
</div>
</core-toolbar>
<div class="changeLog scroll-y" fit>
{{#each changeLogs}}
<paper-shadow class="white padded" style="margin: 8px;">
<h2>{{version}}</h2>
<ul>
{{#each changes}}
<li>{{this}}</li>
{{/each}}
</ul>
</paper-shadow>
{{/each}}
</div>
</template>

View File

@@ -0,0 +1,11 @@
<template name="notFound">
<div layout vertical center center-justified fit>
<h2>The data for the page you requested could not be found.</h2>
{{#if currentUser}}
<h2>It might not exist, or you might not have permission to view it.</h2>
{{else}}
<h2>Perhaps you need to sign in first:</h2>
{{atForm}}
{{/if}}
</div>
</template>

View File

@@ -20,12 +20,12 @@
aria-label="Delete Feature"
noink></paper-icon-button>
{{else}}
{{#unless hideEdit}}
{{#if showEdit}}
<paper-icon-button id="editButton"
icon="create"
aria-label="Delete Feature"
noink></paper-icon-button>
{{/unless}}
{{/if}}
{{/if}}
</core-toolbar>
<div class="detailContent">

View File

@@ -11,6 +11,18 @@ Template.baseDialog.helpers({
editing: function(){
return Template.instance().editing.get();
},
showEdit: function() {
if (this.hideEdit) return false;
var charId = Template.parentData().charId;
if (charId){
var char = Characters.findOne(charId);
var userId = Meteor.userId();
if (char && userId)
return char.owner === userId ||
_.contains(char.writers, userId);
}
return true;
},
});
Template.baseDialog.events({

View File

@@ -3,11 +3,7 @@
<core-toolbar class="blue-grey white-text">
<core-icon-button icon="menu" core-drawer-toggle></core-icon-button>
<div id="username" class="clickable" flex>
{{#if username}}
{{username}}
{{else}}
Tap to set username
{{/if}}
{{profileName}}
</div>
</core-toolbar>
<div id="userProfile" class="padded">
@@ -20,5 +16,7 @@
{{/each}}
</div>
</div>
{{> atForm}}
{{> atNavButton }}
{{/with}}
</template>

View File

@@ -1,3 +1,12 @@
Template.profile.helpers({
profileName: function() {
var user = Meteor.user();
return user.profile && user.profile.username ||
user.username ||
"Tap to set username";
}
});
Template.profile.events({
"tap #username": function(){
if (this._id === Meteor.userId()){

View File

@@ -1,9 +1,8 @@
<template name="usernameDialog">
{{#with currentUser}}
<div>
<paper-input id="usernameInput" label="Username" value={{username}}></paper-input>
</div>
{{/with}}
<div>
<paper-input id="usernameInput" label="Username" value={{profileName}}></paper-input>
</div>
<div style="color: red;" class="vertMargin">{{errorMessage}}</div>
<paper-button id="cancelButton" affirmative> Cancel </paper-button>
<paper-button id="changeButton" affirmative> Change Username </paper-button>
<paper-button id="changeButton" disabled={{invalid}} affirmative> Change Username </paper-button>
</template>

View File

@@ -1,8 +1,49 @@
var getUsername = function() {
var user = Meteor.user();
return user.profile && user.profile.username || user.username;
};
Template.usernameDialog.onCreated(function() {
this.errorMessage = new ReactiveVar("");
this.username = new ReactiveVar(getUsername());
});
Template.usernameDialog.helpers({
profileName: function() {
return getUsername();
},
invalid: function() {
return !!Template.instance().errorMessage.get();
},
errorMessage: function() {
return Template.instance().errorMessage.get();
},
});
Template.usernameDialog.events({
"change #usernameInput, input #usernameInput": function(event, instance) {
var username = instance.find("#usernameInput").value;
username = username.trim().toLowerCase().replace(/\s+/gm, "");
if (username.length < 3){
instance.errorMessage.set("Username too short");
} else {
instance.errorMessage.set("Validating...");
Meteor.call("getUserId", username, function(err, userId){
if (userId && userId !== Meteor.userId())
instance.errorMessage.set("This username is taken");
else
instance.errorMessage.set("");
});
}
},
"tap #changeButton": function(event, instance){
var username = instance.find("#usernameInput").value;
username = username.trim().replace(/\s+/gm, " ");
var profileName = username;
username = username.toLowerCase().replace(/\s+/gm, "");
Meteor.users.update(
Meteor.userId(),
{$set: {username: instance.find("#usernameInput").value}}
{$set: {username: username, "profile.username": profileName}}
);
}
},
});

View File

@@ -0,0 +1,13 @@
<template name="titledAtForm">
<core-toolbar class="blue-grey white-text">
<core-icon-button icon="menu" core-drawer-toggle></core-icon-button>
<div flex>
</div>
</core-toolbar>
<div class="scroll-y padded" fit layout vertical center center-justified>
<paper-shadow class="white" style="max-width: 400px;">
{{> atForm}}
</paper-shadow>
</div>
</template>

View File

@@ -1,3 +1,14 @@
Meteor.methods({
canWriteCharacter: function(charId) {
var userId = this.userId;
var char = Characters.findOne(
charId,
{fields: {owner: 1, writers: 1}}
);
return (userId && char.owner === userId || _.contains(char.writers, userId));
},
});
CHARACTER_SUBSCHEMA_ALLOW = {
// the user must be logged in, and the user must be a writer of the character
insert: function(userId, doc) {

View File

@@ -0,0 +1,71 @@
AccountsTemplates.configure({
//behaviour
confirmPassword: true,
enablePasswordChange: true,
enforceEmailVerification: true,
overrideLoginErrors: false,
sendVerificationEmail: true,
lowercaseUsername: true,
//appearance
continuousValidation: true,
negativeValidation: true,
negativeFeedback: true,
showValidating: true,
showAddRemoveServices: true,
showForgotPasswordLink: true,
showResendVerificationEmailLink: true,
});
AccountsTemplates.configureRoute("changePwd", {
template: "titledAtForm",
});
AccountsTemplates.configureRoute("enrollAccount", {
template: "titledAtForm",
});
AccountsTemplates.configureRoute("forgotPwd", {
template: "titledAtForm",
});
AccountsTemplates.configureRoute("resetPwd", {
template: "titledAtForm",
});
AccountsTemplates.configureRoute("signIn", {
template: "titledAtForm",
});
AccountsTemplates.configureRoute("signUp", {
template: "titledAtForm",
});
AccountsTemplates.configureRoute("verifyEmail", {
template: "titledAtForm",
});
AccountsTemplates.configureRoute("resendVerificationEmail", {
template: "titledAtForm",
});
if (Meteor.isServer){
Meteor.methods({
"userExists": function(username){
return !!Meteor.users.findOne({username: username});
},
});
}
AccountsTemplates.addField({
_id: "username",
type: "text",
required: true,
func: function(value){
if (Meteor.isClient) {
var self = this;
Meteor.call("userExists", value, function(err, userExists){
if (!userExists)
self.setSuccess();
else
self.setError("This username is taken");
self.setValidating(false);
});
return;
}
// Server
return Meteor.call("userExists", value);
},
});

View File

@@ -42,6 +42,12 @@ var inheritParentProperties = function(doc, collection){
"Document's parent does not exist"
);
var handMeDowns = _.pick(parent, collection.inheritedKeys);
if (
_.contains(collection.inheritedKeys, "charId") &&
doc.parent.collection === "Characters"
){
handMeDowns.charId = doc.parent.id;
}
if (_.isEmpty(handMeDowns)) return;
collection.update(doc._id, {$set: handMeDowns});
};
@@ -76,14 +82,13 @@ makeChild = function(collection, inheritedKeys){
}
});
if (Meteor.isClient) {
collection.after.update(function(userId, doc, fieldNames, modifier, options) {
if (modifier && modifier.$set && modifier.$set.parent){
//when we change parents, inherit its properties
inheritParentProperties(doc, collection);
}
});
}
collection.after.update(function(userId, doc, fieldNames, modifier, options) {
if (modifier && modifier.$set && modifier.$set["parent.id"]){
//when we change parents, inherit its properties
console.log("re-inheriting")
inheritParentProperties(doc, collection);
}
});
collection.softRemoveNode = collection.softRemoveNode || function(id){
collection.softRemove(id);
@@ -102,14 +107,12 @@ makeParent = function(collection, donatedKeys){
donatedKeys = joinWithDefaultKeys(donatedKeys);
var collectionName = collection._collection.name;
//after changing, push the changes to all children
if (Meteor.isClient) {
collection.after.update(function(userId, doc, fieldNames, modifier, options) {
modifier = limitModifierToKeys(modifier, donatedKeys);
doc = _.pick(doc, ["_id", "charId"]);
if (!modifier) return;
Meteor.call("updateChildren", doc, modifier, true);
});
}
collection.after.update(function(userId, doc, fieldNames, modifier, options) {
modifier = limitModifierToKeys(modifier, donatedKeys);
doc = _.pick(doc, ["_id", "charId"]);
if (!modifier) return;
Meteor.call("updateChildren", doc, modifier, true);
});
collection.softRemoveNode = function(id){
Meteor.call("softRemoveNode", collectionName, id);
};

View File

@@ -0,0 +1,27 @@
AccountsMeld.configure({
meldDBCallback: function(sourceUserId, destinationUserId){
// Here you can modify every collection you need for the document referencing
// to sourceUserId to be modified in order to point to destinationUserId
Characters.update(
{owner: sourceUserId},
{$set: {owner: destinationUserId}},
{multi: true}
);
Characters.update(
{writers: sourceUserId},
{
$pull: {writers: sourceUserId},
$addToSet: {writers: destinationUserId},
},
{multi: true}
);
Characters.update(
{readers: sourceUserId},
{
$pull: {readers: sourceUserId},
$addToSet: {readers: destinationUserId},
},
{multi: true}
);
},
});

View File

@@ -0,0 +1,3 @@
Meteor.publish("changeLog", function(){
return ChangeLogs.find();
});

View File

@@ -0,0 +1,3 @@
Meteor.publish("user", function(){
return Meteor.users.find(this.userId, {fields: {roles: 1}});
});