Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fbb006783 | ||
|
|
2253672f43 | ||
|
|
ed6d557f8a | ||
|
|
4d642b56bb | ||
|
|
436c5bb785 | ||
|
|
8489ef5ec0 | ||
|
|
c9710bdb09 | ||
|
|
26784f11b6 | ||
|
|
23d43f7d43 | ||
|
|
1ebb0d2527 | ||
|
|
9d86cb8bee | ||
|
|
3343f8a813 | ||
|
|
0260824c2f | ||
|
|
66ee3ff808 | ||
|
|
cb71f6d380 | ||
|
|
2f04d9ec1c | ||
|
|
40c54524a7 | ||
|
|
b890a3b11e | ||
|
|
c9242a95f3 | ||
|
|
fedda62c7c | ||
|
|
612575d0e6 | ||
|
|
d1d22c0d89 | ||
|
|
b94f5ebb4b | ||
|
|
3f32535666 | ||
|
|
4ea02c4fbb | ||
|
|
b052e8dd19 | ||
|
|
e2822b9f22 | ||
|
|
c46b836985 | ||
|
|
65d1bac0dc | ||
|
|
fcae3056de | ||
|
|
7d364c80c0 | ||
|
|
0ff6c08abd | ||
|
|
1c95336843 | ||
|
|
b36720511b | ||
|
|
261220fdd5 | ||
|
|
64edc52cca | ||
|
|
56f1bd2829 | ||
|
|
3b669fd2f9 | ||
|
|
933878e158 | ||
|
|
0e6ca56316 | ||
|
|
6599fe1ef8 | ||
|
|
f39baf43a1 | ||
|
|
96f4e35e25 | ||
|
|
e17dbf6601 | ||
|
|
3f81d419f7 | ||
|
|
1c00f5aa04 | ||
|
|
f5a32cb50a | ||
|
|
f4d3368fb4 | ||
|
|
1de9fb558a | ||
|
|
06ffc94b4c | ||
|
|
74c6a423ee | ||
|
|
41c90bb69f | ||
|
|
68432541db | ||
|
|
c417c45db1 | ||
|
|
3f3caf63e4 | ||
|
|
49c5a1fcb1 | ||
|
|
9fb37148fa | ||
|
|
a67d7fb4ea | ||
|
|
8e0f19742b | ||
|
|
216e502c8a | ||
|
|
1a18d1f816 | ||
|
|
c099e3173b | ||
|
|
de93636c7c | ||
|
|
5e263443b3 | ||
|
|
8c3a891254 | ||
|
|
e737067990 | ||
|
|
465a61f80f | ||
|
|
bbf42aaf97 | ||
|
|
b20d086a24 | ||
|
|
52baf297ca | ||
|
|
45e9f491ff | ||
|
|
742b26b0de | ||
|
|
164ba78c81 | ||
|
|
e27211b24d | ||
|
|
30987752cc | ||
|
|
058ee2691f | ||
|
|
f0cf7f4956 | ||
|
|
75c8720b04 | ||
|
|
f73f2f670f | ||
|
|
c6e62e1cfa | ||
|
|
4e574c0f51 | ||
|
|
80b195b7f7 | ||
|
|
67956d9a42 | ||
|
|
64b3ca6066 | ||
|
|
8349f7da9b | ||
|
|
0636042878 | ||
|
|
6207ffa516 | ||
|
|
9d33612054 | ||
|
|
face6387a0 | ||
|
|
b308595dac | ||
|
|
1d2de197a4 | ||
|
|
a3d790b47d | ||
|
|
efe6dd87db | ||
|
|
5b33a6e783 | ||
|
|
8730fab40b | ||
|
|
992776bb40 | ||
|
|
bc9ec4421c | ||
|
|
4c31ab601c | ||
|
|
c4e77c7eae | ||
|
|
2cd6e27f70 | ||
|
|
f6b2dde479 | ||
|
|
44da62a962 | ||
|
|
4e96047e90 | ||
|
|
212986ac37 | ||
|
|
877f516565 | ||
|
|
750022f0f1 | ||
|
|
614284c73d | ||
|
|
6528fc8bab | ||
|
|
020930b2e4 | ||
|
|
dcd76e06e1 | ||
|
|
8a58002415 | ||
|
|
535fcd77cf | ||
|
|
7c2aed26a4 | ||
|
|
00a050d337 |
64
README.md
64
README.md
@@ -1,13 +1,71 @@
|
||||
RPG Docs
|
||||
DiceCloud
|
||||
========
|
||||
|
||||
This is the repo for [DiceCloud](dicecloud.com).
|
||||
|
||||
DiceCloud is a free, auditable, real-time character sheet for D&D 5e.
|
||||
|
||||
Philosophy
|
||||
----------
|
||||
|
||||
Setting up your character on DiceCloud takes a little longer than
|
||||
just filling it in on a paper character sheet would. The goal of using an
|
||||
online sheet is to make actually playing the game more streamlined, and
|
||||
ultimately more fun. So putting a little extra effort into setting up a
|
||||
character now pays off over and over again once you're playing.
|
||||
|
||||
The idea is to track where each number comes from, and allow you to easily make
|
||||
changes on the fly. Let's look at a hypothetical example.
|
||||
|
||||
> You need to swim through a sunken section of dungeon to fetch the quest's Thing.
|
||||
> You'll need to take off your magical Plate Armor of +1 Constitution to swim
|
||||
> without sinking, of course.
|
||||
>
|
||||
> Taking it off will take away that disadvantage on
|
||||
> stealth checks, change your armor class, your speed and your constitution, and
|
||||
> which in turn changes your hit points and your constitution saving throw.
|
||||
> Working out all those changes in the middle of a game will drag the game to a
|
||||
> halt.
|
||||
>
|
||||
> Fortunately you have DiceCloud, so it's a matter of dragging
|
||||
> your Plate Armor +1 Con from your "equipment" box to your "backpack" box and
|
||||
> you're done. Your hitpoints change correctly, your saving throws are up to date,
|
||||
> your armor class goes back to reflecting the fact that you have natural armor
|
||||
> from being a dragonborn. Your character sheet keeps up and you
|
||||
> ultimately get more time to play the game. Huzzah!
|
||||
|
||||
Getting started
|
||||
---------------
|
||||
|
||||
`git clone https://github.com/ThaumRystra/DiceCloud1 dicecloud`
|
||||
Running DiceCloud locally, either to host it yourself away from an internet
|
||||
connection, or to contribute to developing it further, is fairly
|
||||
straightforward and it should work on Linux, Windows, and Mac.
|
||||
|
||||
You'll need to have installed:
|
||||
|
||||
- [git](https://www.atlassian.com/git/tutorials/install-git)
|
||||
- [Meteor](https://www.meteor.com/install)
|
||||
- [Bower](https://bower.io/)
|
||||
|
||||
Then, it's just a matter of cloning this repository into a folder, installing the bower dependencies and running
|
||||
`meteor` in the app directory.
|
||||
|
||||
`git clone https://github.com/ThaumRystra/DiceCloud dicecloud`
|
||||
`cd dicecloud`
|
||||
`cd rpg-docs`
|
||||
`cd app`
|
||||
`bower install`
|
||||
`meteor`
|
||||
|
||||
You should see this:
|
||||
|
||||
```
|
||||
=> Started proxy.
|
||||
=> Started MongoDB.
|
||||
=> Started your app.
|
||||
|
||||
=> App running at: http://localhost:3000/
|
||||
```
|
||||
|
||||
Now, visiting http://localhost:3000/ should show you an empty instance of
|
||||
DiceCloud running.
|
||||
|
||||
|
||||
3
rpg-docs/.gitignore → app/.gitignore
vendored
3
rpg-docs/.gitignore → app/.gitignore
vendored
@@ -4,6 +4,9 @@
|
||||
settings.json
|
||||
public/components
|
||||
public/_imports.html
|
||||
private/oldClient
|
||||
nohup.out
|
||||
node_modules
|
||||
dump
|
||||
.idea/
|
||||
.cache
|
||||
@@ -13,3 +13,6 @@ notices-for-facebook-graph-api-2
|
||||
1.3.0-split-minifiers-package
|
||||
1.4.0-remove-old-dev-bundle-link
|
||||
1.4.1-add-shell-server-package
|
||||
1.4.3-split-account-service-packages
|
||||
1.5-add-dynamic-import-package
|
||||
1.7-split-underscore-from-meteor-base
|
||||
@@ -3,10 +3,9 @@
|
||||
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||
# but you can also edit it by hand.
|
||||
|
||||
iron:router
|
||||
accounts-password@1.3.3
|
||||
accounts-ui@1.1.9
|
||||
random@1.0.10
|
||||
accounts-password@1.5.1
|
||||
accounts-ui@1.3.1
|
||||
random@1.1.0
|
||||
dburles:collection-helpers
|
||||
reactive-var@1.0.11
|
||||
underscore@1.0.10
|
||||
@@ -18,35 +17,40 @@ dburles:mongo-collection-instances
|
||||
percolate:migrations
|
||||
ecwyne:mathjs
|
||||
useraccounts:polymer
|
||||
accounts-google@1.0.11
|
||||
accounts-google@1.3.2
|
||||
splendido:accounts-meld
|
||||
email@1.1.18
|
||||
email@1.2.3
|
||||
meteorhacks:subs-manager
|
||||
chuangbo:marked
|
||||
reywood:iron-router-ga
|
||||
meteor-base@1.0.4
|
||||
mobile-experience@1.0.4
|
||||
mongo@1.1.14
|
||||
meteor-base@1.4.0
|
||||
mobile-experience@1.0.5
|
||||
mongo@1.6.0
|
||||
blaze-html-templates
|
||||
session@1.1.7
|
||||
session@1.1.8
|
||||
jquery@1.11.10
|
||||
tracker@1.1.1
|
||||
logging@1.1.16
|
||||
reload@1.1.11
|
||||
ejson@1.0.13
|
||||
tracker@1.2.0
|
||||
logging@1.1.20
|
||||
reload@1.2.0
|
||||
ejson@1.1.0
|
||||
spacebars
|
||||
check@1.2.4
|
||||
check@1.3.1
|
||||
useraccounts:iron-routing
|
||||
wizonesolutions:canonical
|
||||
standard-minifier-js@1.2.1
|
||||
shell-server@0.2.1
|
||||
standard-minifier-js@2.4.0
|
||||
shell-server@0.4.0
|
||||
seba:minifiers-autoprefixer
|
||||
nikogosovd:multiple-uihooks
|
||||
templates:array
|
||||
ecmascript@0.6.1
|
||||
es5-shim@4.6.15
|
||||
ecmascript@0.12.0
|
||||
es5-shim@4.8.0
|
||||
differential:vulcanize
|
||||
reactive-dict
|
||||
percolate:synced-cron
|
||||
reactive-dict@1.2.1
|
||||
ongoworks:speakingurl
|
||||
service-configuration
|
||||
service-configuration@1.0.11
|
||||
google-config-ui@1.0.1
|
||||
dynamic-import@0.5.0
|
||||
ddp-rate-limiter@1.0.7
|
||||
rate-limit@1.0.9
|
||||
iron:router
|
||||
littledata:synced-cron
|
||||
1
app/.meteor/release
Normal file
1
app/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
METEOR@1.8
|
||||
139
app/.meteor/versions
Normal file
139
app/.meteor/versions
Normal file
@@ -0,0 +1,139 @@
|
||||
accounts-base@1.4.3
|
||||
accounts-google@1.3.2
|
||||
accounts-oauth@1.1.16
|
||||
accounts-password@1.5.1
|
||||
accounts-ui@1.3.1
|
||||
accounts-ui-unstyled@1.4.1
|
||||
aldeed:collection2@2.10.0
|
||||
aldeed:collection2-core@1.2.0
|
||||
aldeed:schema-deny@1.1.0
|
||||
aldeed:schema-index@1.1.1
|
||||
aldeed:simple-schema@1.5.4
|
||||
allow-deny@1.1.0
|
||||
autoupdate@1.5.0
|
||||
babel-compiler@7.2.0
|
||||
babel-runtime@1.3.0
|
||||
base64@1.0.11
|
||||
binary-heap@1.0.11
|
||||
blaze@2.3.3
|
||||
blaze-html-templates@1.1.2
|
||||
blaze-tools@1.0.10
|
||||
boilerplate-generator@1.6.0
|
||||
caching-compiler@1.2.0
|
||||
caching-html-compiler@1.1.3
|
||||
callback-hook@1.1.0
|
||||
check@1.3.1
|
||||
chuangbo:marked@0.3.5_1
|
||||
coffeescript@1.0.17
|
||||
dburles:collection-helpers@1.1.0
|
||||
dburles:mongo-collection-instances@0.3.5
|
||||
ddp@1.4.0
|
||||
ddp-client@2.3.3
|
||||
ddp-common@1.4.0
|
||||
ddp-rate-limiter@1.0.7
|
||||
ddp-server@2.2.0
|
||||
deps@1.0.12
|
||||
diff-sequence@1.1.0
|
||||
differential:vulcanize@3.0.0
|
||||
dynamic-import@0.5.0
|
||||
ecmascript@0.12.0
|
||||
ecmascript-runtime@0.7.0
|
||||
ecmascript-runtime-client@0.8.0
|
||||
ecmascript-runtime-server@0.7.1
|
||||
ecwyne:mathjs@0.25.0
|
||||
ejson@1.1.0
|
||||
email@1.2.3
|
||||
es5-shim@4.8.0
|
||||
fetch@0.1.0
|
||||
geojson-utils@1.0.10
|
||||
google-config-ui@1.0.1
|
||||
google-oauth@1.2.6
|
||||
hot-code-push@1.0.4
|
||||
html-tools@1.0.11
|
||||
htmljs@1.0.11
|
||||
http@1.4.1
|
||||
id-map@1.1.0
|
||||
inter-process-messaging@0.1.0
|
||||
iron:controller@1.0.12
|
||||
iron:core@1.0.11
|
||||
iron:dynamic-template@1.0.12
|
||||
iron:layout@1.0.12
|
||||
iron:location@1.0.11
|
||||
iron:middleware-stack@1.1.0
|
||||
iron:router@1.1.2
|
||||
iron:url@1.1.0
|
||||
jquery@1.11.11
|
||||
lai:collection-extensions@0.2.1_1
|
||||
launch-screen@1.1.1
|
||||
less@2.8.0
|
||||
littledata:synced-cron@1.5.1
|
||||
livedata@1.0.18
|
||||
localstorage@1.2.0
|
||||
logging@1.1.20
|
||||
matb33:collection-hooks@0.8.4
|
||||
mdg:validation-error@0.5.1
|
||||
meteor@1.9.2
|
||||
meteor-base@1.4.0
|
||||
meteorhacks:subs-manager@1.6.4
|
||||
minifier-css@1.4.0
|
||||
minifier-js@2.4.0
|
||||
minimongo@1.4.5
|
||||
mobile-experience@1.0.5
|
||||
mobile-status-bar@1.0.14
|
||||
modern-browsers@0.1.2
|
||||
modules@0.13.0
|
||||
modules-runtime@0.10.2
|
||||
momentjs:moment@2.22.2
|
||||
mongo@1.6.0
|
||||
mongo-decimal@0.1.0
|
||||
mongo-dev-server@1.1.0
|
||||
mongo-id@1.0.7
|
||||
nikogosovd:multiple-uihooks@0.1.8
|
||||
npm-bcrypt@0.9.3
|
||||
npm-mongo@3.1.1
|
||||
oauth@1.2.3
|
||||
oauth2@1.2.1
|
||||
observe-sequence@1.0.16
|
||||
ongoworks:speakingurl@9.0.0
|
||||
ordered-dict@1.1.0
|
||||
percolate:migrations@0.9.8
|
||||
promise@0.11.1
|
||||
raix:eventemitter@0.1.3
|
||||
random@1.1.0
|
||||
rate-limit@1.0.9
|
||||
reactive-dict@1.2.1
|
||||
reactive-var@1.0.11
|
||||
reload@1.2.0
|
||||
retry@1.1.0
|
||||
reywood:iron-router-ga@0.7.1
|
||||
routepolicy@1.1.0
|
||||
seba:minifiers-autoprefixer@1.1.1
|
||||
service-configuration@1.0.11
|
||||
session@1.1.8
|
||||
sha@1.0.9
|
||||
shell-server@0.4.0
|
||||
socket-stream-client@0.2.2
|
||||
softwarerero:accounts-t9n@1.3.11
|
||||
spacebars@1.0.15
|
||||
spacebars-compiler@1.1.3
|
||||
splendido:accounts-emails-field@1.2.0
|
||||
splendido:accounts-meld@1.3.1
|
||||
srp@1.0.12
|
||||
standard-minifier-js@2.4.0
|
||||
templates:array@1.0.3
|
||||
templating@1.3.2
|
||||
templating-compiler@1.3.3
|
||||
templating-runtime@1.3.2
|
||||
templating-tools@1.1.2
|
||||
tracker@1.2.0
|
||||
ui@1.0.13
|
||||
underscore@1.0.10
|
||||
url@1.2.0
|
||||
useraccounts:core@1.14.2
|
||||
useraccounts:iron-routing@1.14.2
|
||||
useraccounts:polymer@1.14.2
|
||||
webapp@1.7.0
|
||||
webapp-hashing@1.0.9
|
||||
wizonesolutions:canonical@0.0.5
|
||||
zimme:collection-behaviours@1.1.3
|
||||
zimme:collection-softremovable@1.0.5
|
||||
@@ -65,3 +65,37 @@ makeParent(Buffs, ["name", "enabled"]); //parents of effects, attacks, proficien
|
||||
|
||||
Buffs.allow(CHARACTER_SUBSCHEMA_ALLOW);
|
||||
Buffs.deny(CHARACTER_SUBSCHEMA_DENY);
|
||||
|
||||
Meteor.methods({
|
||||
applyBuff: function(buffId, targetId){
|
||||
if (!Meteor.call("canWriteCharacter", targetId)){
|
||||
throw new Meteor.Error(
|
||||
"Access denied",
|
||||
"You do not have permission to buff this character"
|
||||
);
|
||||
}
|
||||
let buff = CustomBuffs.findOne(buffId);
|
||||
if (!buff) return;
|
||||
|
||||
var parentCol = Meteor.isClient ?
|
||||
window[buff.parent.collection] : global[buff.parent.collection]
|
||||
var parent = parentCol.findOne(buff.parent.id);
|
||||
|
||||
//insert new buff
|
||||
newBuffId = Buffs.insert({
|
||||
charId: targetId,
|
||||
name: buff.name,
|
||||
description: buff.description,
|
||||
lifeTime: {total: buff.lifeTime.total},
|
||||
type: "custom",
|
||||
|
||||
appliedBy: buff.charId,
|
||||
appliedByDetails: {
|
||||
name: parent && parent.name || "",
|
||||
collection: buff.parent.collection,
|
||||
},
|
||||
});
|
||||
|
||||
Meteor.call("cloneChildren", buffId, {id: newBuffId, collection: "Buffs"})
|
||||
}
|
||||
})
|
||||
@@ -164,9 +164,9 @@ Schemas.Character = new SimpleSchema({
|
||||
|
||||
//permissions
|
||||
party: {type: String, regEx: SimpleSchema.RegEx.Id, optional: true},
|
||||
owner: {type: String, regEx: SimpleSchema.RegEx.Id},
|
||||
readers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: []},
|
||||
writers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: []},
|
||||
owner: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
|
||||
readers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: [], index: 1},
|
||||
writers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: [], index: 1},
|
||||
color: {
|
||||
type: String,
|
||||
allowedValues: _.pluck(colorOptions, "key"),
|
||||
@@ -185,11 +185,13 @@ Schemas.Character = new SimpleSchema({
|
||||
type: String,
|
||||
defaultValue: "whitelist",
|
||||
allowedValues: ["whitelist", "public"],
|
||||
index: 1,
|
||||
},
|
||||
"settings.swapStatAndModifier": {type: Boolean, defaultValue: false},
|
||||
"settings.exportFeatures": {type: Boolean, defaultValue: true},
|
||||
"settings.exportAttacks": {type: Boolean, defaultValue: true},
|
||||
"settings.exportDescription": {type: Boolean, defaultValue: true},
|
||||
"settings.newUserExperience": {type: Boolean, optional: true},
|
||||
});
|
||||
|
||||
Characters.attachSchema(Schemas.Character);
|
||||
@@ -297,6 +299,7 @@ Characters.calculate = {
|
||||
var fieldSelector = {};
|
||||
fieldSelector[fieldName] = 1;
|
||||
var char = Characters.findOne(charId, {fields: fieldSelector});
|
||||
if (!char) return;
|
||||
var field = char[fieldName];
|
||||
if (field === undefined){
|
||||
throw new Meteor.Error(
|
||||
@@ -330,6 +333,7 @@ Characters.calculate = {
|
||||
},
|
||||
attributeValue: memoize(function(charId, attributeName){
|
||||
var attribute = Characters.calculate.getField(charId, attributeName);
|
||||
if (!attribute) return;
|
||||
//base value
|
||||
var value = Characters.calculate.attributeBase(charId, attributeName);
|
||||
//plus adjustment
|
||||
@@ -341,6 +345,7 @@ Characters.calculate = {
|
||||
}),
|
||||
skillMod: memoize(preventLoop(function(charId, skillName){
|
||||
var skill = Characters.calculate.getField(charId, skillName);
|
||||
if (!skill) return;
|
||||
//get the final value of the ability score
|
||||
var ability = Characters.calculate.attributeValue(charId, skill.ability);
|
||||
|
||||
@@ -392,7 +397,6 @@ Characters.calculate = {
|
||||
return prof && prof.value || 0;
|
||||
}),
|
||||
passiveSkill: memoize(function(charId, skillName){
|
||||
var skill = Characters.calculate.getField(charId, skillName);
|
||||
var mod = +Characters.calculate.skillMod(charId, skillName);
|
||||
var value = 10 + mod;
|
||||
Effects.find(
|
||||
@@ -553,6 +557,10 @@ if (Meteor.isServer){
|
||||
});
|
||||
Characters.before.insert(function(userId, doc) {
|
||||
doc.urlName = getSlug(doc.name, {maintainCase: true}) || "-";
|
||||
// The first character a user creates should have the new user experience
|
||||
if (!Characters.find({owner: userId}).count()){
|
||||
doc.settings.newUserExperience = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -574,8 +582,8 @@ Characters.allow({
|
||||
});
|
||||
|
||||
Characters.deny({
|
||||
update: function(userId, docs, fields, modifier) {
|
||||
// can't change owners
|
||||
return _.contains(fields, "owner");
|
||||
update: function(userId, doc, fields, modifier) {
|
||||
// can't change owners unless you are the current owner
|
||||
return _.contains(fields, "owner") && doc.owner !== userId;
|
||||
}
|
||||
});
|
||||
@@ -2,14 +2,38 @@ Libraries = new Mongo.Collection("library");
|
||||
|
||||
Schemas.Library = new SimpleSchema({
|
||||
name: {type: String},
|
||||
owner: {type: String, regEx: SimpleSchema.RegEx.Id},
|
||||
readers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: []},
|
||||
writers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: []},
|
||||
public: {type: Boolean, defaultValue: false},
|
||||
owner: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
|
||||
readers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: [], index: 1},
|
||||
writers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: [], index: 1},
|
||||
public: {type: Boolean, defaultValue: false, index: 1},
|
||||
});
|
||||
|
||||
Libraries.attachSchema(Schemas.Library);
|
||||
|
||||
Libraries.after.remove(function(userId, library) {
|
||||
LibraryItems.remove({library: library._id});
|
||||
LibrarySpells.remove({library: library._id});
|
||||
});
|
||||
|
||||
Meteor.methods({
|
||||
removeLibrary: function(libraryId) {
|
||||
let library = Libraries.findOne(libraryId);
|
||||
let userId = Meteor.userId();
|
||||
|
||||
if (!library) return;
|
||||
if (library.owner === userId){
|
||||
Libraries.remove(libraryId);
|
||||
} else {
|
||||
if (_.contains(library.readers, userId)){
|
||||
Libraries.update(libraryId, {$pull: {"readers": userId}});
|
||||
}
|
||||
if (_.contains(library.writers, userId)){
|
||||
Libraries.update(libraryId, {$pull: {"writers": userId}});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Libraries.allow({
|
||||
insert(userId, doc) {
|
||||
return userId && doc.owner === userId;
|
||||
@@ -18,16 +42,14 @@ Libraries.allow({
|
||||
return canEdit(userId, doc);
|
||||
},
|
||||
remove(userId, doc) {
|
||||
return canEdit(userId, doc);
|
||||
return userId && doc.owner === userId;
|
||||
},
|
||||
fetch: ["owner", "writers"],
|
||||
});
|
||||
|
||||
Libraries.deny({
|
||||
// For now, only admins can manage libraries
|
||||
insert(userId, doc){
|
||||
var user = Meteor.users.findOne(userId);
|
||||
return !user || !_.contains(user.roles, "admin");
|
||||
return !Meteor.users.findOne(userId);
|
||||
},
|
||||
update(userId, doc, fields, modifier) {
|
||||
// Can't change owners
|
||||
@@ -1,7 +1,7 @@
|
||||
Schemas.LibraryAttacks = new SimpleSchema({
|
||||
name: {
|
||||
type: String,
|
||||
defaultValue: "New Attack",
|
||||
optional: true,
|
||||
trim: false,
|
||||
},
|
||||
details: {
|
||||
@@ -9,7 +9,6 @@ Schemas.LibraryEffects = new SimpleSchema({
|
||||
defaultValue: "add",
|
||||
allowedValues: [
|
||||
"base",
|
||||
"proficiency",
|
||||
"add",
|
||||
"mul",
|
||||
"min",
|
||||
91
app/Model/Library/LibraryItems.js
Normal file
91
app/Model/Library/LibraryItems.js
Normal file
@@ -0,0 +1,91 @@
|
||||
LibraryItems = new Mongo.Collection("libraryItems");
|
||||
|
||||
Schemas.LibraryItems = new SimpleSchema({
|
||||
libraryName:{type: String, optional: true, trim: false},
|
||||
name: {type: String, defaultValue: "New Item", trim: false},
|
||||
plural: {type: String, optional: true, trim: false},
|
||||
description:{type: String, optional: true, trim: false},
|
||||
quantity: {type: Number, min: 0, defaultValue: 1},
|
||||
weight: {type: Number, min: 0, defaultValue: 0, decimal: true},
|
||||
value: {type: Number, min: 0, defaultValue: 0, decimal: true},
|
||||
requiresAttunement: {type: Boolean, defaultValue: false},
|
||||
|
||||
library: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
|
||||
|
||||
"settings.category": {
|
||||
type: String,
|
||||
optional: true,
|
||||
allowedValues: [
|
||||
"adventuringGear", "armor", "weapons", "tools",
|
||||
],
|
||||
},
|
||||
"settings.showIncrement": {
|
||||
type: Boolean,
|
||||
defaultValue: false,
|
||||
},
|
||||
|
||||
effects: {type: [Schemas.LibraryEffects], defaultValue: []},
|
||||
attacks: {type: [Schemas.LibraryAttacks], defaultValue: []},
|
||||
});
|
||||
|
||||
LibraryItems.attachSchema(Schemas.LibraryItems);
|
||||
|
||||
LibraryItems.allow({
|
||||
insert(userId, doc) {
|
||||
return Libraries.canEdit(userId, doc.library);
|
||||
},
|
||||
update(userId, doc, fields, modifier) {
|
||||
return Libraries.canEdit(userId, doc.library);
|
||||
},
|
||||
remove(userId, doc) {
|
||||
return Libraries.canEdit(userId, doc.library);
|
||||
},
|
||||
fetch: ["library"],
|
||||
});
|
||||
|
||||
Meteor.methods({
|
||||
updateLibraryItemEffect: function({itemId, effectIndex, field, value, unsetField}){
|
||||
let libraryId = LibraryItems.findOne(itemId).library;
|
||||
let userId = Meteor.userId();
|
||||
if (!Libraries.canEdit(userId, libraryId)) return;
|
||||
let modifier = {
|
||||
$set: {
|
||||
[`effects.${effectIndex}.${field}`]: value,
|
||||
}
|
||||
};
|
||||
if (unsetField){
|
||||
modifier.$unset = {
|
||||
[`effects.${effectIndex}.${unsetField}`]: 1,
|
||||
}
|
||||
}
|
||||
LibraryItems.update(itemId, modifier);
|
||||
},
|
||||
removeLibraryItemEffect: function({itemId, effectIndex}){
|
||||
let libraryId = LibraryItems.findOne(itemId).library;
|
||||
let userId = Meteor.userId();
|
||||
if (!Libraries.canEdit(userId, libraryId)) return;
|
||||
LibraryItems.update(itemId, {$unset : {
|
||||
[`effects.${effectIndex}`] : 1,
|
||||
}});
|
||||
LibraryItems.update(itemId, {$pull : {"effects" : null}});
|
||||
},
|
||||
updateLibraryItemAttack: function({itemId, attackIndex, field, value}){
|
||||
let libraryId = LibraryItems.findOne(itemId).library;
|
||||
let userId = Meteor.userId();
|
||||
if (!Libraries.canEdit(userId, libraryId)) return;
|
||||
LibraryItems.update(itemId, {
|
||||
$set: {
|
||||
[`attacks.${attackIndex}.${field}`]: value,
|
||||
}
|
||||
});
|
||||
},
|
||||
removeLibraryItemAttack: function({itemId, attackIndex}){
|
||||
let libraryId = LibraryItems.findOne(itemId).library;
|
||||
let userId = Meteor.userId();
|
||||
if (!Libraries.canEdit(userId, libraryId)) return;
|
||||
LibraryItems.update(itemId, {$unset : {
|
||||
[`attacks.${attackIndex}`] : 1,
|
||||
}});
|
||||
LibraryItems.update(itemId, {$pull : {"attacks" : null}});
|
||||
},
|
||||
})
|
||||
9
app/Model/Meta/Blacklist.js
Normal file
9
app/Model/Meta/Blacklist.js
Normal file
@@ -0,0 +1,9 @@
|
||||
Blacklist = new Mongo.Collection("blacklist");
|
||||
|
||||
Schemas.Blacklist = new SimpleSchema({
|
||||
userId: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
Blacklist.attachSchema(Schemas.Blacklist);
|
||||
26
app/Model/Meta/PatreonPosts.js
Normal file
26
app/Model/Meta/PatreonPosts.js
Normal file
@@ -0,0 +1,26 @@
|
||||
PatreonPosts = new Mongo.Collection("patreonPosts");
|
||||
|
||||
Schemas.PatreonPosts = new SimpleSchema({
|
||||
link: {
|
||||
type: String,
|
||||
},
|
||||
dateAdded: {
|
||||
type: Date,
|
||||
autoValue(){
|
||||
return new Date();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
PatreonPosts.attachSchema(Schemas.PatreonPosts);
|
||||
|
||||
PatreonPosts.allow({
|
||||
insert: function(userId, doc) {
|
||||
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");
|
||||
},
|
||||
});
|
||||
121
app/Model/Users/Users.js
Normal file
121
app/Model/Users/Users.js
Normal file
@@ -0,0 +1,121 @@
|
||||
Schemas.UserProfile = new SimpleSchema({
|
||||
username: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
librarySubscriptions: {
|
||||
type: [String],
|
||||
defaultValue: [],
|
||||
},
|
||||
});
|
||||
|
||||
Schemas.User = new SimpleSchema({
|
||||
username: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
profile: {
|
||||
type: Schemas.UserProfile,
|
||||
optional: true,
|
||||
},
|
||||
emails: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
},
|
||||
"emails.$": {
|
||||
type: Object,
|
||||
},
|
||||
"emails.$.address": {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Email,
|
||||
},
|
||||
"emails.$.verified": {
|
||||
type: Boolean,
|
||||
},
|
||||
registered_emails: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
},
|
||||
"registered_emails.$": {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
},
|
||||
createdAt: {
|
||||
type: Date
|
||||
},
|
||||
services: {
|
||||
type: Object,
|
||||
optional: true,
|
||||
blackbox: true,
|
||||
},
|
||||
roles: {
|
||||
type: Object,
|
||||
optional: true,
|
||||
blackbox: true,
|
||||
},
|
||||
roles: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
},
|
||||
"roles.$": {
|
||||
type: String
|
||||
},
|
||||
// In order to avoid an 'Exception in setInterval callback' from Meteor
|
||||
heartbeat: {
|
||||
type: Date,
|
||||
optional: true,
|
||||
},
|
||||
apiKey: {
|
||||
type: String,
|
||||
index: 1,
|
||||
optional: true,
|
||||
},
|
||||
lastPatreonPostClicked: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
|
||||
Meteor.users.attachSchema(Schemas.User);
|
||||
|
||||
Meteor.users.allow({
|
||||
update: function(userId, doc, fields, modifier) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Meteor.isServer) Meteor.methods({
|
||||
generateMyApiKey() {
|
||||
var user = Meteor.users.findOne(this.userId);
|
||||
if (!user) return;
|
||||
if (user && user.apiKey) return;
|
||||
var apiKey = Random.id(30);
|
||||
Meteor.users.update(this.userId, {$set: {apiKey}});
|
||||
},
|
||||
});
|
||||
|
||||
Meteor.methods({
|
||||
clickPatreonPost(link) {
|
||||
Meteor.users.update(this.userId, {$set: {
|
||||
lastPatreonPostClicked: link
|
||||
}});
|
||||
},
|
||||
});
|
||||
268
app/Routes/API.js
Normal file
268
app/Routes/API.js
Normal file
@@ -0,0 +1,268 @@
|
||||
Router.map(function () {
|
||||
this.route("vmixCharacter", {
|
||||
path: "/vmix-character/:_id/",
|
||||
where: "server",
|
||||
action: function () {
|
||||
this.response.setHeader("Content-Type", "application/json");
|
||||
var query = this.params.query;
|
||||
var key = query && query.key;
|
||||
ifKeyValid(key, this.response, "vmixCharacter", () =>
|
||||
this.response.end(vMixCharacter(this.params._id))
|
||||
);
|
||||
},
|
||||
});
|
||||
this.route("vmixParty", {
|
||||
path: "/vmix-party/:_id/",
|
||||
where: "server",
|
||||
action: function () {
|
||||
this.response.setHeader("Content-Type", "application/json");
|
||||
var query = this.params.query;
|
||||
var key = query && query.key;
|
||||
ifKeyValid(key, this.response, "vmixParty", () =>
|
||||
this.response.end(vMixParty(this.params._id))
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
this.route("jsonCharacterSheet", { // GET /character/:_id/json?key=:key
|
||||
path: "/character/:_id/json",
|
||||
where: "server",
|
||||
action: function () {
|
||||
this.response.setHeader("Content-Type", "application/json");
|
||||
var query = this.params.query;
|
||||
var key = query && query.key;
|
||||
ifKeyValid(key, this.response, "jsonCharacterSheet", () => {
|
||||
if (canViewCharacter(this.params._id, userIdFromKey(key))) {
|
||||
this.response.end(JSONExport(this.params._id))
|
||||
} else {
|
||||
this.response.writeHead(403, "You do not have permission to view this character");
|
||||
this.response.end();
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
this.route("getUserId", { // GET /api/user?username=:un&key=:key
|
||||
path: "/api/user",
|
||||
where: "server",
|
||||
action: function () {
|
||||
this.response.setHeader("Content-Type", "application/json");
|
||||
var query = this.params.query;
|
||||
var key = query && query.key;
|
||||
var username = query && query.username;
|
||||
ifKeyValid(key, this.response, "getUserId", () => {
|
||||
Meteor.call("getUserId", username, (err, result) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
this.response.writeHead(404, "User not found");
|
||||
this.response.end();
|
||||
} else {
|
||||
console.log(result);
|
||||
this.response.end(JSON.stringify({id: result}));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.route("addSpellsToCharacter", { // POST /api/character/:_id/spellList/:listId
|
||||
path: "/api/character/:_id/spellList/:listId",
|
||||
where: "server"
|
||||
}).post(function () {
|
||||
const key = startPOSTResponse(this);
|
||||
const spells = this.request.body;
|
||||
const charId = this.params._id;
|
||||
const listId = this.params.listId;
|
||||
Meteor.call("insertSpells", key, charId, listId, spells, (err, res) => {
|
||||
if (err) {
|
||||
this.response.writeHead(err.error, err.reason);
|
||||
this.response.end(err.details);
|
||||
} else {
|
||||
this.response.end(JSON.stringify(res));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.route("createCharacter", { // POST /api/character
|
||||
path: "/api/character",
|
||||
where: "server"
|
||||
}).post(function () {
|
||||
const key = startPOSTResponse(this);
|
||||
const character = this.request.body;
|
||||
Meteor.call("insertCharacter", key, character, (err, res) => {
|
||||
if (err) {
|
||||
this.response.writeHead(err.error, err.reason);
|
||||
this.response.end(err.details);
|
||||
} else {
|
||||
this.response.end(JSON.stringify(res));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.route("deleteCharacter", { // DELETE /api/character/:_id
|
||||
path: "/api/character/:_id",
|
||||
where: "server"
|
||||
}).delete(function () {
|
||||
const key = startPOSTResponse(this);
|
||||
const charId = this.params._id;
|
||||
Meteor.call("deleteCharacter", key, charId, (err, res) => {
|
||||
if (err) {
|
||||
this.response.writeHead(err.error, err.reason);
|
||||
this.response.end(err.details);
|
||||
} else {
|
||||
this.response.end(JSON.stringify(res));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.route("transferCharacterOwnership", { // PUT /api/character/:_id/owner
|
||||
path: "/api/character/:_id/owner",
|
||||
where: "server"
|
||||
}).put(function () {
|
||||
const key = startPOSTResponse(this);
|
||||
const charId = this.params._id;
|
||||
const ownerId = this.request.body['id'];
|
||||
Meteor.call("transferCharacterOwnership", key, charId, ownerId, (err, res) => {
|
||||
if (err) {
|
||||
this.response.writeHead(err.error, err.reason);
|
||||
this.response.end(err.details);
|
||||
} else {
|
||||
this.response.end(JSON.stringify(res));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.route("insertFeatures", { // POST /api/character/:_id/feature
|
||||
path: "/api/character/:_id/feature",
|
||||
where: "server",
|
||||
}).post(function () {
|
||||
const key = startPOSTResponse(this);
|
||||
const charId = this.params._id;
|
||||
const features = this.request.body;
|
||||
Meteor.call("insertFeatures", key, charId, features, (err, res) => {
|
||||
if (err) {
|
||||
this.response.writeHead(err.error, err.reason);
|
||||
this.response.end(err.details);
|
||||
} else {
|
||||
this.response.end(JSON.stringify(res));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.route("insertProfs", { // POST /api/character/:_id/prof
|
||||
path: "/api/character/:_id/prof",
|
||||
where: "server",
|
||||
}).post(function () {
|
||||
const key = startPOSTResponse(this);
|
||||
const charId = this.params._id;
|
||||
const profs = this.request.body;
|
||||
Meteor.call("insertProfs", key, charId, profs, (err, res) => {
|
||||
if (err) {
|
||||
this.response.writeHead(err.error, err.reason);
|
||||
this.response.end(err.details);
|
||||
} else {
|
||||
this.response.end(JSON.stringify(res));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.route("insertEffects", { // POST /api/character/:_id/effect
|
||||
path: "/api/character/:_id/effect",
|
||||
where: "server",
|
||||
}).post(function () {
|
||||
const key = startPOSTResponse(this);
|
||||
const charId = this.params._id;
|
||||
const effects = this.request.body;
|
||||
Meteor.call("insertEffects", key, charId, effects, (err, res) => {
|
||||
if (err) {
|
||||
this.response.writeHead(err.error, err.reason);
|
||||
this.response.end(err.details);
|
||||
} else {
|
||||
this.response.end(JSON.stringify(res));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.route("insertClasses", { // POST /api/character/:_id/class
|
||||
path: "/api/character/:_id/class",
|
||||
where: "server",
|
||||
}).post(function () {
|
||||
const key = startPOSTResponse(this);
|
||||
const charId = this.params._id;
|
||||
const classes = this.request.body;
|
||||
Meteor.call("insertClasses", key, charId, classes, (err, res) => {
|
||||
if (err) {
|
||||
this.response.writeHead(err.error, err.reason);
|
||||
this.response.end(err.details);
|
||||
} else {
|
||||
this.response.end(JSON.stringify(res));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const startPOSTResponse = function (request) {
|
||||
request.response.setHeader("Content-Type", "application/json");
|
||||
const header = request.request.headers;
|
||||
return header && header['authorization'];
|
||||
};
|
||||
|
||||
var ifKeyValid = function (apiKey, response, method, callback) {
|
||||
if (!apiKey) {
|
||||
response.writeHead(403, "You must use an api key to access this api");
|
||||
response.end();
|
||||
} else if (!isKeyValid(apiKey)) {
|
||||
response.writeHead(403, "API key is invalid");
|
||||
response.end();
|
||||
} else if (isRateLimited(apiKey, method)) {
|
||||
response.writeHead(429, "Too many requests");
|
||||
response.end(JSON.stringify({
|
||||
"timeToReset": rateLimiter.check({apiKey: apiKey, method: method}).timeToReset
|
||||
}));
|
||||
} else {
|
||||
rateLimiter.increment({apiKey: apiKey, method: method});
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
isKeyValid = function (apiKey) {
|
||||
var user = Meteor.users.findOne({apiKey});
|
||||
if (!user) return false;
|
||||
var blackListed = Blacklist.findOne({userId: user._id});
|
||||
return !blackListed;
|
||||
};
|
||||
|
||||
userIdFromKey = function (apiKey) {
|
||||
var user = Meteor.users.findOne({apiKey}); // we know user exists from isKeyValid
|
||||
return user._id;
|
||||
};
|
||||
|
||||
rateLimiter = new RateLimiter();
|
||||
// global limit
|
||||
rateLimiter.addRule({apiKey: String}, 10, 1000);
|
||||
|
||||
// vmix stuff
|
||||
rateLimiter.addRule({apiKey: String, method: "vmixCharacter"}, 2, 10000);
|
||||
rateLimiter.addRule({apiKey: String, method: "vmixParty"}, 2, 10000);
|
||||
|
||||
// bot API endpoints
|
||||
rateLimiter.addRule({apiKey: String, method: "jsonCharacterSheet"}, 5, 5000);
|
||||
rateLimiter.addRule({apiKey: String, method: "getUserId"}, 5, 5000);
|
||||
rateLimiter.addRule({apiKey: String, method: "addSpellsToCharacter"}, 5, 5000);
|
||||
rateLimiter.addRule({apiKey: String, method: "createCharacter"}, 5, 5000);
|
||||
rateLimiter.addRule({apiKey: String, method: "transferCharacterOwnership"}, 5, 5000);
|
||||
rateLimiter.addRule({apiKey: String, method: "insertFeatures"}, 5, 5000);
|
||||
rateLimiter.addRule({apiKey: String, method: "insertProfs"}, 5, 5000);
|
||||
rateLimiter.addRule({apiKey: String, method: "insertEffects"}, 5, 5000);
|
||||
rateLimiter.addRule({apiKey: String, method: "insertClasses"}, 5, 5000);
|
||||
|
||||
isRateLimited = function (apiKey, method) {
|
||||
const limited = !rateLimiter.check({apiKey: apiKey, method: method}).allowed;
|
||||
if (limited) {
|
||||
console.log(`Rate limit hit by API key ${apiKey}`);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -13,6 +13,11 @@ Router.plugin("ensureSignedIn", {
|
||||
|
||||
Router.plugin("dataNotFound", {notFoundTemplate: "notFound"});
|
||||
|
||||
var handleSubError = function(e){
|
||||
Session.set("error", {reason: e.reason, href: location.href});
|
||||
Router.go("/error");
|
||||
};
|
||||
|
||||
Router.map(function() {
|
||||
this.route("/", {
|
||||
name: "home",
|
||||
@@ -36,7 +41,9 @@ Router.map(function() {
|
||||
path: "/character/:_id/",
|
||||
waitOn: function(){
|
||||
return [
|
||||
subsManager.subscribe("singleCharacter", this.params._id),
|
||||
subsManager.subscribe(
|
||||
"singleCharacter", this.params._id, {onError: handleSubError}
|
||||
),
|
||||
];
|
||||
},
|
||||
action: function(){
|
||||
@@ -52,7 +59,9 @@ Router.map(function() {
|
||||
path: "/character/:_id/:urlName",
|
||||
waitOn: function(){
|
||||
return [
|
||||
subsManager.subscribe("singleCharacter", this.params._id),
|
||||
subsManager.subscribe(
|
||||
"singleCharacter", this.params._id, {onError: handleSubError}
|
||||
),
|
||||
];
|
||||
},
|
||||
data: function() {
|
||||
@@ -78,10 +87,41 @@ Router.map(function() {
|
||||
fastRender: true,
|
||||
});
|
||||
|
||||
this.route("printedCharacterSheet", {
|
||||
path: "/character/:_id/:urlName/print",
|
||||
waitOn: function(){
|
||||
return [
|
||||
subsManager.subscribe(
|
||||
"singleCharacter", this.params._id, {onError: handleSubError}
|
||||
),
|
||||
];
|
||||
},
|
||||
data: function() {
|
||||
var data = Characters.findOne(
|
||||
{_id: this.params._id},
|
||||
{fields: {_id: 1, name: 1, color: 1, writers: 1, readers: 1}}
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onAfterAction: function() {
|
||||
var char = Characters.findOne({_id: this.params._id}, {fields: {name: 1}});
|
||||
var name = char && char.name;
|
||||
if (name){
|
||||
document.title = name + " - Printing";
|
||||
}
|
||||
},
|
||||
//analytics
|
||||
trackPageView: false,
|
||||
onRun: function() {
|
||||
window.ga && window.ga("send", "pageview", "/print-character");
|
||||
this.next();
|
||||
},
|
||||
});
|
||||
|
||||
this.route("library", {
|
||||
path: "/library",
|
||||
waitOn: function(){
|
||||
return subsManager.subscribe("standardLibraries");
|
||||
return subsManager.subscribe("customLibraries");
|
||||
},
|
||||
onAfterAction: function() {
|
||||
document.title = appName + " - Library";
|
||||
@@ -124,4 +164,11 @@ Router.map(function() {
|
||||
document.title = appName;
|
||||
},
|
||||
});
|
||||
|
||||
this.route("/error", {
|
||||
name: "error",
|
||||
onAfterAction: function() {
|
||||
document.title = `${appName} - Error`;
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "RPG Docs",
|
||||
"version": "0.0.0",
|
||||
"name": "dicecloud",
|
||||
"version": "1",
|
||||
"homepage": "",
|
||||
"authors": [
|
||||
"Stefan Zermatten"
|
||||
174
app/client/compatibility/jquery.quickfit.js
Normal file
174
app/client/compatibility/jquery.quickfit.js
Normal file
@@ -0,0 +1,174 @@
|
||||
// jscs:disable
|
||||
// https://github.com/chunksnbits/jquery-quickfit
|
||||
(function ($) {
|
||||
var Quickfit, QuickfitHelper, defaults, pluginName;
|
||||
|
||||
pluginName = 'quickfit';
|
||||
|
||||
defaults = {
|
||||
min: 8,
|
||||
max: 12,
|
||||
tolerance: 0.02,
|
||||
truncate: false,
|
||||
width: null,
|
||||
sampleNumberOfLetters: 10,
|
||||
sampleFontSize: 12
|
||||
};
|
||||
QuickfitHelper = (function () {
|
||||
|
||||
var sharedInstance = null;
|
||||
|
||||
QuickfitHelper.instance = function (options) {
|
||||
if (!sharedInstance) {
|
||||
sharedInstance = new QuickfitHelper(options);
|
||||
}
|
||||
return sharedInstance;
|
||||
};
|
||||
|
||||
function QuickfitHelper(options) {
|
||||
this.options = options;
|
||||
|
||||
this.item = $('<span id="meassure"></span>');
|
||||
this.item.css({
|
||||
position: 'absolute',
|
||||
left: '-1000px',
|
||||
top: '-1000px',
|
||||
'font-size': "" + this.options.sampleFontSize + "px"
|
||||
});
|
||||
$('body').append(this.item);
|
||||
|
||||
this.meassures = {};
|
||||
}
|
||||
|
||||
QuickfitHelper.prototype.getMeassure = function (letter) {
|
||||
var currentMeassure;
|
||||
currentMeassure = this.meassures[letter];
|
||||
if (!currentMeassure) {
|
||||
currentMeassure = this.setMeassure(letter);
|
||||
}
|
||||
return currentMeassure;
|
||||
};
|
||||
|
||||
QuickfitHelper.prototype.setMeassure = function (letter) {
|
||||
var currentMeassure, index, sampleLetter, text, _ref;
|
||||
|
||||
text = '';
|
||||
sampleLetter = letter === ' ' ? ' ' : letter;
|
||||
|
||||
for (index = 0, _ref = this.options.sampleNumberOfLetters - 1; 0 <= _ref ? index <= _ref : index >= _ref; 0 <= _ref ? index++ : index--) {
|
||||
text += sampleLetter;
|
||||
}
|
||||
|
||||
this.item.html(text);
|
||||
currentMeassure = this.item.width() / this.options.sampleNumberOfLetters / this.options.sampleFontSize;
|
||||
this.meassures[letter] = currentMeassure;
|
||||
|
||||
return currentMeassure;
|
||||
};
|
||||
|
||||
return QuickfitHelper;
|
||||
|
||||
})();
|
||||
|
||||
Quickfit = (function () {
|
||||
|
||||
function Quickfit(element, options) {
|
||||
this.$element = element;
|
||||
this.options = $.extend({}, defaults, options);
|
||||
this.$element = $(this.$element);
|
||||
this._defaults = defaults;
|
||||
this._name = pluginName;
|
||||
this.quickfitHelper = QuickfitHelper.instance(this.options);
|
||||
}
|
||||
|
||||
Quickfit.prototype.fit = function () {
|
||||
var elementWidth;
|
||||
if (!this.options.width) {
|
||||
elementWidth = this.$element.width();
|
||||
this.options.width = elementWidth - this.options.tolerance * elementWidth;
|
||||
}
|
||||
if (this.text = this.$element.attr('data-quickfit')) {
|
||||
this.previouslyTruncated = true;
|
||||
} else {
|
||||
this.text = this.$element.text();
|
||||
}
|
||||
this.calculateFontSize();
|
||||
|
||||
if (this.options.truncate) this.truncate();
|
||||
|
||||
return {
|
||||
$element: this.$element,
|
||||
size: this.fontSize
|
||||
};
|
||||
};
|
||||
|
||||
Quickfit.prototype.calculateFontSize = function () {
|
||||
var letter, textWidth, i;
|
||||
|
||||
textWidth = 0;
|
||||
for (i = 0; i < this.text.length; ++i) {
|
||||
letter = this.text.charAt(i);
|
||||
textWidth += this.quickfitHelper.getMeassure(letter);
|
||||
}
|
||||
|
||||
this.targetFontSize = parseInt(this.options.width / textWidth);
|
||||
return this.fontSize = Math.max(this.options.min, Math.min(this.options.max, this.targetFontSize));
|
||||
};
|
||||
|
||||
Quickfit.prototype.truncate = function () {
|
||||
var index, lastLetter, letter, textToAdd, textWidth;
|
||||
|
||||
if (this.fontSize > this.targetFontSize) {
|
||||
textToAdd = '';
|
||||
textWidth = 3 * this.quickfitHelper.getMeassure('.') * this.fontSize;
|
||||
|
||||
index = 0;
|
||||
while (textWidth < this.options.width && index < this.text.length) {
|
||||
letter = this.text[index++];
|
||||
if (lastLetter) textToAdd += lastLetter;
|
||||
textWidth += this.fontSize * this.quickfitHelper.getMeassure(letter);
|
||||
lastLetter = letter;
|
||||
}
|
||||
|
||||
if (textToAdd.length + 1 === this.text.length) {
|
||||
textToAdd = this.text;
|
||||
} else {
|
||||
textToAdd += '...';
|
||||
}
|
||||
this.textWasTruncated = true;
|
||||
|
||||
return this.$element.attr('data-quickfit', this.text).html(textToAdd);
|
||||
|
||||
} else {
|
||||
if (this.previouslyTruncated) {
|
||||
return this.$element.html(this.text);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return Quickfit;
|
||||
|
||||
})();
|
||||
|
||||
return $.fn.quickfit = function (options) {
|
||||
var measurements = [];
|
||||
|
||||
// Separate measurements from repaints
|
||||
// First calculate all measurements...
|
||||
var $elements = this.each(function () {
|
||||
var measurement = new Quickfit(this, options).fit();
|
||||
measurements.push(measurement);
|
||||
return measurement.$element;
|
||||
});
|
||||
|
||||
// ... then apply the measurements.
|
||||
for (var i = 0; i < measurements.length; i++) {
|
||||
var measurement = measurements[i];
|
||||
|
||||
measurement.$element.css({ fontSize: measurement.size + 'px' });
|
||||
}
|
||||
|
||||
return $elements;
|
||||
};
|
||||
|
||||
})(jQuery, window);
|
||||
@@ -3,7 +3,8 @@ Template.registerHelper("canEditCharacter", function(charId) {
|
||||
});
|
||||
|
||||
canEditCharacter = function(charId) {
|
||||
var char = Characters.findOne(charId)
|
||||
var char = Characters.findOne(charId);
|
||||
if (!char) return false;
|
||||
var userId = Meteor.userId();
|
||||
return char.owner === userId ||
|
||||
_.contains(char.writers, userId);
|
||||
5
app/client/lib/fixPasswordButton.js
Normal file
5
app/client/lib/fixPasswordButton.js
Normal file
@@ -0,0 +1,5 @@
|
||||
let pwdFormSubmit = AccountsTemplates.atPwdFormEvents["submit #at-pwd-form"]
|
||||
|
||||
Template.atPwdForm.events({
|
||||
"click .at-btn.submit": pwdFormSubmit,
|
||||
});
|
||||
12
app/client/lib/printing.js
Normal file
12
app/client/lib/printing.js
Normal file
@@ -0,0 +1,12 @@
|
||||
Session.setDefault("isPrinting", false);
|
||||
if (window.matchMedia) {
|
||||
var mediaQueryList = window.matchMedia("print");
|
||||
mediaQueryList.addListener(function(mql) {
|
||||
if (mql.matches) {
|
||||
Session.set("isPrinting", true);
|
||||
Tracker.flush();
|
||||
} else {
|
||||
Session.set("isPrinting", false);
|
||||
}
|
||||
});
|
||||
}
|
||||
17
app/client/lib/removeDuplicateProficiencies.js
Normal file
17
app/client/lib/removeDuplicateProficiencies.js
Normal file
@@ -0,0 +1,17 @@
|
||||
removeDuplicateProficiencies = function(proficiencies) {
|
||||
dict = {};
|
||||
proficiencies.forEach(function(prof) {
|
||||
if (prof.name in dict) { //if we have already gone over another proficiency for the same thing
|
||||
if (dict[prof.name].value < prof.value) {
|
||||
dict[prof.name] = prof; //then take the new one if it's higher, otherwise leave it
|
||||
}
|
||||
} else {
|
||||
dict[prof.name] = prof; //if it wasn't already there, store it
|
||||
}
|
||||
});
|
||||
profs = []
|
||||
_.forEach(dict, function(prof) {
|
||||
profs.push(prof);
|
||||
})
|
||||
return profs;
|
||||
};
|
||||
17
app/client/style/bounce.css
Normal file
17
app/client/style/bounce.css
Normal file
@@ -0,0 +1,17 @@
|
||||
@keyframes bounce {
|
||||
from {
|
||||
transform: translate(0px,0px);
|
||||
}
|
||||
to {
|
||||
transform: translate(0px,-16px);
|
||||
}
|
||||
}
|
||||
|
||||
.bounce{
|
||||
animation-name: bounce;
|
||||
animation-duration: 0.3s;
|
||||
animation-direction: alternate;
|
||||
animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
animation-delay: 0s;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
8
app/client/style/iosButtonFix.css
Normal file
8
app/client/style/iosButtonFix.css
Normal file
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
* iOS doens't believe in click events for some elements.
|
||||
* This is here to convince it to allow buttons to be clickable
|
||||
*/
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user