Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45e9f491ff | ||
|
|
742b26b0de | ||
|
|
164ba78c81 | ||
|
|
e27211b24d | ||
|
|
30987752cc | ||
|
|
058ee2691f | ||
|
|
f0cf7f4956 | ||
|
|
75c8720b04 | ||
|
|
f73f2f670f | ||
|
|
c6e62e1cfa | ||
|
|
4e574c0f51 | ||
|
|
80b195b7f7 | ||
|
|
67956d9a42 | ||
|
|
64b3ca6066 | ||
|
|
8349f7da9b | ||
|
|
0636042878 | ||
|
|
6207ffa516 | ||
|
|
9d33612054 | ||
|
|
face6387a0 | ||
|
|
b308595dac | ||
|
|
00a050d337 |
@@ -3,9 +3,8 @@
|
||||
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||
# but you can also edit it by hand.
|
||||
|
||||
iron:router
|
||||
accounts-password@1.4.0
|
||||
accounts-ui@1.1.9
|
||||
accounts-password@1.5.0
|
||||
accounts-ui@1.2.0
|
||||
random@1.0.10
|
||||
dburles:collection-helpers
|
||||
reactive-var@1.0.11
|
||||
@@ -18,38 +17,40 @@ dburles:mongo-collection-instances
|
||||
percolate:migrations
|
||||
ecwyne:mathjs
|
||||
useraccounts:polymer
|
||||
accounts-google@1.2.0
|
||||
accounts-google@1.3.0
|
||||
splendido:accounts-meld
|
||||
email@1.2.3
|
||||
meteorhacks:subs-manager
|
||||
chuangbo:marked
|
||||
reywood:iron-router-ga
|
||||
meteor-base@1.1.0
|
||||
mobile-experience@1.0.4
|
||||
mongo@1.2.0
|
||||
meteor-base@1.2.0
|
||||
mobile-experience@1.0.5
|
||||
mongo@1.3.1
|
||||
blaze-html-templates
|
||||
session@1.1.7
|
||||
jquery@1.11.10
|
||||
tracker@1.1.3
|
||||
logging@1.1.17
|
||||
logging@1.1.19
|
||||
reload@1.1.11
|
||||
ejson@1.0.14
|
||||
ejson@1.1.0
|
||||
spacebars
|
||||
check@1.2.5
|
||||
useraccounts:iron-routing
|
||||
wizonesolutions:canonical
|
||||
standard-minifier-js@2.1.1
|
||||
shell-server@0.2.4
|
||||
standard-minifier-js@2.2.0
|
||||
shell-server@0.3.0
|
||||
seba:minifiers-autoprefixer
|
||||
nikogosovd:multiple-uihooks
|
||||
templates:array
|
||||
ecmascript@0.8.2
|
||||
ecmascript@0.9.0
|
||||
es5-shim@4.6.15
|
||||
differential:vulcanize
|
||||
reactive-dict@1.1.9
|
||||
reactive-dict@1.2.0
|
||||
percolate:synced-cron
|
||||
ongoworks:speakingurl
|
||||
service-configuration@1.0.11
|
||||
google-config-ui
|
||||
dynamic-import
|
||||
ddp-rate-limiter
|
||||
google-config-ui@1.0.0
|
||||
dynamic-import@0.2.0
|
||||
ddp-rate-limiter@1.0.7
|
||||
rate-limit@1.0.8
|
||||
iron:router
|
||||
|
||||
@@ -1 +1 @@
|
||||
METEOR@1.5.2
|
||||
METEOR@1.6.0.1
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
accounts-base@1.3.3
|
||||
accounts-google@1.2.0
|
||||
accounts-base@1.4.2
|
||||
accounts-google@1.3.1
|
||||
accounts-oauth@1.1.15
|
||||
accounts-password@1.4.0
|
||||
accounts-ui@1.1.9
|
||||
accounts-ui-unstyled@1.2.1
|
||||
accounts-password@1.5.0
|
||||
accounts-ui@1.2.0
|
||||
accounts-ui-unstyled@1.3.0
|
||||
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.3
|
||||
allow-deny@1.0.9
|
||||
allow-deny@1.1.0
|
||||
autoupdate@1.3.12
|
||||
babel-compiler@6.20.0
|
||||
babel-runtime@1.0.1
|
||||
babel-compiler@6.24.7
|
||||
babel-runtime@1.1.1
|
||||
base64@1.0.10
|
||||
binary-heap@1.0.10
|
||||
blaze@2.3.2
|
||||
blaze-html-templates@1.1.2
|
||||
blaze-tools@1.0.10
|
||||
boilerplate-generator@1.2.0
|
||||
caching-compiler@1.1.9
|
||||
boilerplate-generator@1.3.1
|
||||
caching-compiler@1.1.11
|
||||
caching-html-compiler@1.1.2
|
||||
callback-hook@1.0.10
|
||||
check@1.2.5
|
||||
@@ -27,31 +27,30 @@ chuangbo:marked@0.3.5_1
|
||||
coffeescript@1.11.1_4
|
||||
dburles:collection-helpers@1.1.0
|
||||
dburles:mongo-collection-instances@0.3.5
|
||||
ddp@1.3.1
|
||||
ddp-client@2.1.3
|
||||
ddp-common@1.2.9
|
||||
ddp@1.4.0
|
||||
ddp-client@2.2.0
|
||||
ddp-common@1.3.0
|
||||
ddp-rate-limiter@1.0.7
|
||||
ddp-server@2.0.2
|
||||
ddp-server@2.1.1
|
||||
deps@1.0.12
|
||||
diff-sequence@1.0.7
|
||||
differential:vulcanize@3.0.0
|
||||
dynamic-import@0.1.3
|
||||
ecmascript@0.8.2
|
||||
ecmascript-runtime@0.4.1
|
||||
ecmascript-runtime-client@0.4.3
|
||||
ecmascript-runtime-server@0.4.1
|
||||
dynamic-import@0.2.1
|
||||
ecmascript@0.9.0
|
||||
ecmascript-runtime@0.5.0
|
||||
ecmascript-runtime-client@0.5.0
|
||||
ecmascript-runtime-server@0.5.0
|
||||
ecwyne:mathjs@0.25.0
|
||||
ejson@1.0.14
|
||||
ejson@1.1.0
|
||||
email@1.2.3
|
||||
es5-shim@4.6.15
|
||||
fastclick@1.0.13
|
||||
geojson-utils@1.0.10
|
||||
google-config-ui@1.0.0
|
||||
google-oauth@1.2.4
|
||||
google-oauth@1.2.5
|
||||
hot-code-push@1.0.4
|
||||
html-tools@1.0.11
|
||||
htmljs@1.0.11
|
||||
http@1.2.12
|
||||
http@1.3.0
|
||||
id-map@1.0.9
|
||||
iron:controller@1.0.12
|
||||
iron:core@1.0.11
|
||||
@@ -64,41 +63,41 @@ iron:url@1.1.0
|
||||
jquery@1.11.10
|
||||
lai:collection-extensions@0.2.1_1
|
||||
launch-screen@1.1.1
|
||||
less@2.7.9
|
||||
less@2.7.12
|
||||
livedata@1.0.18
|
||||
localstorage@1.1.1
|
||||
logging@1.1.17
|
||||
localstorage@1.2.0
|
||||
logging@1.1.19
|
||||
matb33:collection-hooks@0.8.4
|
||||
mdg:validation-error@0.5.1
|
||||
meteor@1.7.2
|
||||
meteor-base@1.1.0
|
||||
meteor@1.8.2
|
||||
meteor-base@1.2.0
|
||||
meteorhacks:subs-manager@1.6.4
|
||||
minifier-css@1.2.16
|
||||
minifier-js@2.1.3
|
||||
minimongo@1.3.1
|
||||
mobile-experience@1.0.4
|
||||
minifier-js@2.2.2
|
||||
minimongo@1.4.3
|
||||
mobile-experience@1.0.5
|
||||
mobile-status-bar@1.0.14
|
||||
modules@0.10.0
|
||||
modules-runtime@0.8.0
|
||||
momentjs:moment@2.18.1
|
||||
mongo@1.2.2
|
||||
mongo-dev-server@1.0.1
|
||||
modules@0.11.3
|
||||
modules-runtime@0.9.2
|
||||
momentjs:moment@2.20.1
|
||||
mongo@1.3.1
|
||||
mongo-dev-server@1.1.0
|
||||
mongo-id@1.0.6
|
||||
nikogosovd:multiple-uihooks@0.1.8
|
||||
npm-bcrypt@0.9.3
|
||||
npm-mongo@2.2.30
|
||||
oauth@1.1.13
|
||||
oauth2@1.1.11
|
||||
npm-mongo@2.2.34
|
||||
oauth@1.2.1
|
||||
oauth2@1.2.0
|
||||
observe-sequence@1.0.16
|
||||
ongoworks:speakingurl@9.0.0
|
||||
ordered-dict@1.0.9
|
||||
percolate:migrations@0.9.8
|
||||
percolate:synced-cron@1.3.2
|
||||
promise@0.9.0
|
||||
promise@0.10.1
|
||||
raix:eventemitter@0.1.3
|
||||
random@1.0.10
|
||||
rate-limit@1.0.8
|
||||
reactive-dict@1.1.9
|
||||
reactive-dict@1.2.0
|
||||
reactive-var@1.0.11
|
||||
reload@1.1.11
|
||||
retry@1.0.9
|
||||
@@ -108,14 +107,14 @@ seba:minifiers-autoprefixer@1.0.1
|
||||
service-configuration@1.0.11
|
||||
session@1.1.7
|
||||
sha@1.0.9
|
||||
shell-server@0.2.4
|
||||
shell-server@0.3.1
|
||||
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.10
|
||||
standard-minifier-js@2.1.1
|
||||
standard-minifier-js@2.2.3
|
||||
templates:array@1.0.3
|
||||
templating@1.3.2
|
||||
templating-compiler@1.3.3
|
||||
@@ -128,7 +127,7 @@ url@1.1.0
|
||||
useraccounts:core@1.14.2
|
||||
useraccounts:iron-routing@1.14.2
|
||||
useraccounts:polymer@1.14.2
|
||||
webapp@1.3.19
|
||||
webapp@1.4.0
|
||||
webapp-hashing@1.0.9
|
||||
wizonesolutions:canonical@0.0.5
|
||||
zimme:collection-behaviours@1.1.3
|
||||
|
||||
9
rpg-docs/Model/Meta/Blacklist.js
Normal file
9
rpg-docs/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);
|
||||
@@ -1,3 +1,75 @@
|
||||
Schemas.UserProfile = new SimpleSchema({
|
||||
username: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
Meteor.users.attachSchema(Schemas.User);
|
||||
|
||||
Meteor.users.allow({
|
||||
update: function(userId, doc, fields, modifier) {
|
||||
if (
|
||||
@@ -21,3 +93,13 @@ Meteor.users.allow({
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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}});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,7 +4,11 @@ Router.map(function() {
|
||||
where: "server",
|
||||
action: function() {
|
||||
this.response.setHeader("Content-Type", "application/json");
|
||||
this.response.end(vMixCharacter(this.params._id));
|
||||
var query = this.params.query;
|
||||
var key = query && query.key;
|
||||
ifKeyValid(key, this.response, () =>
|
||||
this.response.end(vMixCharacter(this.params._id))
|
||||
);
|
||||
},
|
||||
});
|
||||
this.route("vmixParty", {
|
||||
@@ -12,7 +16,47 @@ Router.map(function() {
|
||||
where: "server",
|
||||
action: function() {
|
||||
this.response.setHeader("Content-Type", "application/json");
|
||||
this.response.end(vMixParty(this.params._id));
|
||||
var query = this.params.query;
|
||||
var key = query && query.key;
|
||||
ifKeyValid(key, this.response, () =>
|
||||
this.response.end(vMixParty(this.params._id))
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
var ifKeyValid = function(apiKey, response, 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)){
|
||||
response.writeHead(429, "Too many requests");
|
||||
response.end();
|
||||
} else {
|
||||
rateLimiter.increment({apiKey})
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
var isKeyValid = function(apiKey){
|
||||
var user = Meteor.users.findOne({apiKey});
|
||||
if (!user) return false;
|
||||
var blackListed = Blacklist.findOne({userId: user._id});
|
||||
return !blackListed;
|
||||
};
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.addRule({apiKey: String}, 2, 10000);
|
||||
|
||||
var isRateLimited = function(apiKey){
|
||||
const limited = !rateLimiter.check({apiKey}).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,6 +87,37 @@ 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(){
|
||||
@@ -124,4 +164,11 @@ Router.map(function() {
|
||||
document.title = appName;
|
||||
},
|
||||
});
|
||||
|
||||
this.route("/error", {
|
||||
name: "error",
|
||||
onAfterAction: function() {
|
||||
document.title = `${appName} - Error`;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
174
rpg-docs/client/compatibility/jquery.quickfit.js
Normal file
174
rpg-docs/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);
|
||||
12
rpg-docs/client/lib/printing.js
Normal file
12
rpg-docs/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
rpg-docs/client/lib/removeDuplicateProficiencies.js
Normal file
17
rpg-docs/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;
|
||||
};
|
||||
@@ -20,6 +20,12 @@
|
||||
<iron-icon icon="social:share" item-icon></iron-icon>
|
||||
Share
|
||||
</paper-icon-item>
|
||||
<a href={{printUrl}}>
|
||||
<paper-icon-item id="printButton">
|
||||
<iron-icon icon="print" item-icon></iron-icon>
|
||||
Print
|
||||
</paper-icon-item>
|
||||
</a>
|
||||
<paper-icon-item id="characterSettings">
|
||||
<iron-icon icon="settings" item-icon></iron-icon>
|
||||
Settings
|
||||
|
||||
@@ -165,6 +165,12 @@ var getTab = function(charId){
|
||||
};
|
||||
|
||||
Template.characterSheet.helpers({
|
||||
printing: function(){
|
||||
return Session.get("isPrinting");
|
||||
},
|
||||
printUrl: function(){
|
||||
return `/character/${this._id}/${this.urlName || "-"}/print`
|
||||
},
|
||||
selectedTab: function(){
|
||||
return getTab(this._id);
|
||||
},
|
||||
@@ -181,8 +187,8 @@ Template.characterSheet.helpers({
|
||||
const step = Session.get("newUserExperienceStep");
|
||||
if (selected == tab) return false;
|
||||
return (tab === 1 && step === 0) ||
|
||||
(tab === 5 && step === 1) ||
|
||||
(tab === 0 && step === 2);
|
||||
(tab === 5 && step === 1) ||
|
||||
(tab === 0 && step === 2);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -133,6 +133,12 @@
|
||||
<div class="right clickable flex layout horizontal center">
|
||||
{{title}}
|
||||
</div>
|
||||
<div class="layout horizontal center">
|
||||
<div class="layout vertical">
|
||||
<paper-button class="resourceResetMax" disabled={{cantIncrement}}>Reset</paper-button>
|
||||
<paper-button class="resourceResetZero" disabled={{cantDecrement}}>Clear</paper-button>
|
||||
</div>
|
||||
</div>
|
||||
</paper-material>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
@@ -1,21 +1,3 @@
|
||||
var 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;
|
||||
};
|
||||
|
||||
Template.features.helpers({
|
||||
features: function(){
|
||||
var features = Features.find({charId: this._id}, {sort: {color: 1, name: 1}});
|
||||
@@ -129,6 +111,17 @@ Template.resource.helpers({
|
||||
});
|
||||
|
||||
Template.resource.events({
|
||||
"click .resourceResetMax": function(event){
|
||||
var modifier = {$set: {}};
|
||||
modifier.$set[this.name + ".adjustment"] = 0;
|
||||
Characters.update(this.char._id, modifier, {validate: false});
|
||||
},
|
||||
"click .resourceResetZero": function(event){
|
||||
var base = Characters.calculate.attributeBase(this.char._id, this.name);
|
||||
var modifier = {$set: {}};
|
||||
modifier.$set[this.name + ".adjustment"] = -base;
|
||||
Characters.update(this.char._id, modifier, {validate: false});
|
||||
},
|
||||
"click .resourceUp": function(event){
|
||||
var value = Characters.calculate.attributeValue(this.char._id, this.name);
|
||||
var base = Characters.calculate.attributeBase(this.char._id, this.name);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.printedAbility .title.paper-font-subhead {
|
||||
font-size: 2.5mm !important;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<template name="printedAbility">
|
||||
<div class="printedAbility layout vertical center double-border">
|
||||
<div class="paper-font-subhead title flex layout horizontal center">
|
||||
{{title}}
|
||||
</div>
|
||||
<div class="paper-font-display1 stat">
|
||||
{{characterCalculate "attributeValue" ../_id ability}}
|
||||
</div>
|
||||
<div class="paper-font-subhead modifier">
|
||||
{{abilityMod}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
Template.printedAbility.helpers({
|
||||
abilityMod: function() {
|
||||
return signedString(
|
||||
Characters.calculate.abilityMod(
|
||||
Template.parentData()._id, this.ability
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
<template name="printedAttack">
|
||||
<div class="printedAttack" style="margin-bottom: 2mm">
|
||||
<div class="layout horizontal">
|
||||
<div class="paper-font-headline layout horizontal center"
|
||||
style="margin-right: 1mm; min-width: 32px; text-align: right;">
|
||||
{{evaluateAttackBonus charId attack}}
|
||||
</div>
|
||||
<div class="flex layout vertical">
|
||||
<div class="paper-font-body2">
|
||||
{{attack.name}}
|
||||
</div>
|
||||
<div>
|
||||
{{evaluateDamage charId attack}} {{attack.damageType}}
|
||||
</div>
|
||||
{{#if attack.details}}
|
||||
<div>
|
||||
{{attack.details}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
Template.printedAttack.helpers({
|
||||
evaluateAttackBonus: function(charId, attack) {
|
||||
if (attack.parent.collection == "Spells") {
|
||||
var spell = Spells.findOne(attack.parent.id);
|
||||
if (spell) {
|
||||
bonus = evaluate(charId, attack.attackBonus, {
|
||||
"spellListId": spell.parent.id
|
||||
});
|
||||
}
|
||||
} else {
|
||||
var bonus = evaluate(charId, attack.attackBonus);
|
||||
}
|
||||
|
||||
if (_.isFinite(bonus)) {
|
||||
return bonus > 0 ? "+" + bonus : "" + bonus;
|
||||
} else {
|
||||
return bonus;
|
||||
}
|
||||
},
|
||||
evaluateDamage: function(charId, attack) {
|
||||
if (attack.parent.collection == "Spells") {
|
||||
var spell = Spells.findOne(attack.parent.id);
|
||||
if (spell) {
|
||||
return evaluateSpellString(charId, spell.parent.id, attack.damage);
|
||||
}
|
||||
} else {
|
||||
return evaluateString(charId, attack.damage);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,190 @@
|
||||
.printed .page {
|
||||
width: 100%;
|
||||
padding: 6mm;
|
||||
page-break-inside: avoid;
|
||||
page-break-after: always;
|
||||
}
|
||||
|
||||
.printed .shrink-to-fit {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.printed p {
|
||||
margin-bottom: 1mm;
|
||||
}
|
||||
|
||||
.printed .double-border {
|
||||
position: relative;
|
||||
padding: 11px 10px;
|
||||
}
|
||||
|
||||
.printed .double-border > * {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.printed .double-border:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border: 16px solid transparent;
|
||||
border-image-source: url(/png/doubleLineImageBorder.png);
|
||||
border-image-slice: 110 126 fill;
|
||||
border-image-repeat: stretch;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.printed .double-border.printedAbility {
|
||||
padding: 11px 6px 0;
|
||||
margin-bottom: 3mm;
|
||||
}
|
||||
|
||||
.printed .double-border.printedAbility:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.printed .printedAbility .modifier {
|
||||
position: relative;
|
||||
top: 4px;
|
||||
z-index: 1;
|
||||
padding: 2px 18px;
|
||||
background-image: url(/png/upwardPointingBorder.png);
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
print-color-adjust: exact;
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
|
||||
.printed .octogon-border {
|
||||
position: relative;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.printed .octogon-border:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border: 22px solid transparent;
|
||||
border-image: url(/png/octogonBorder.png) 124 118 fill;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.printed iron-icon {
|
||||
width: 16px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
.printed .proficiencies, .printed .attacks, .printed .background {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.printed .shield-background {
|
||||
background: url(/png/shieldBorder.png);
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
print-color-adjust: exact;
|
||||
-webkit-print-color-adjust: exact;
|
||||
padding: 4px 8px 8px;
|
||||
width: 80px;
|
||||
height: 91px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.printed .shield-background .paper-font-subhead {
|
||||
width: 64px;
|
||||
text-align: center;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.printed {
|
||||
font-size: 3mm;
|
||||
}
|
||||
|
||||
.printed .paper-font-body2 {
|
||||
font-size: 3mm;
|
||||
line-height: 4mm;
|
||||
}
|
||||
|
||||
.printed .paper-font-subhead {
|
||||
font-size: 3mm !important;
|
||||
line-height: 3.5mm !important;
|
||||
font-weight: bold !important;
|
||||
text-transform: uppercase !important;
|
||||
}
|
||||
|
||||
.printed .paper-font-subhead.modifier {
|
||||
font-size: 4mm !important;
|
||||
line-height: 6mm !important;
|
||||
}
|
||||
|
||||
.printed .paper-font-display1 {
|
||||
font-size: 7mm !important;
|
||||
line-height: 12mm !important;
|
||||
}
|
||||
|
||||
.printed .paper-font-headline {
|
||||
font-size: 5mm !important;
|
||||
line-height: 6mm !important;
|
||||
}
|
||||
|
||||
.printed .lined-background {
|
||||
background-image: url(/png/horizontalLine.png);
|
||||
background-size: 100% 4mm;
|
||||
print-color-adjust: exact;
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
|
||||
@media screen {
|
||||
.printed .page {
|
||||
width: 210mm;
|
||||
height: 297mm;
|
||||
background: white;
|
||||
margin: 8px;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
|
||||
0 1px 5px 0 rgba(0, 0, 0, 0.12),
|
||||
0 3px 1px -2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.printed .page-holder {
|
||||
width: calc(210mm + 16px);
|
||||
}
|
||||
|
||||
.printed {
|
||||
overflow: auto;
|
||||
padding-left:
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
app-drawer {
|
||||
display: none;
|
||||
}
|
||||
app-header {
|
||||
display: none;
|
||||
}
|
||||
.printed {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 99;
|
||||
background: #fff;
|
||||
}
|
||||
.printed .page-holder {
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.printed .page {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
<template name="printedCharacterSheet">
|
||||
<div class="fit printed-character-sheet layout vertical">
|
||||
<app-header fixed effects="waterfall">
|
||||
<app-toolbar class="medium {{colorClass}} layout horizontal center" style="z-index: 2;">
|
||||
<paper-icon-button icon="menu" drawer-toggle></paper-icon-button>
|
||||
<paper-icon-button icon="arrow-back" class="backButton"></paper-icon-button>
|
||||
<div class="flex character-name">
|
||||
{{name}}
|
||||
</div>
|
||||
<div style="position: relative;">
|
||||
<paper-icon-button icon="print" class="printButton"></paper-icon-button>
|
||||
{{#simpleTooltip}} Print {{/simpleTooltip}}
|
||||
</div>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
<div class="printed flex">
|
||||
<div class="page-holder">
|
||||
<div class="page">
|
||||
<div class="layout vertical" style="height: 100%;">
|
||||
<div class="layout horizontal center" style="margin-bottom: 4mm">
|
||||
<img src="/crown-dice-logo-cropped-transparent.png" style="width: 60px; margin-right: 2mm">
|
||||
<div class="characterName paper-font-title" style="margin-right: 4mm">
|
||||
{{name}}
|
||||
</div>
|
||||
<div class="paper-font-body2">
|
||||
<div>
|
||||
{{#each classes}}
|
||||
<span style="margin-right: 2mm;">
|
||||
{{name}} {{level}}
|
||||
</span>
|
||||
{{/each}}
|
||||
</div>
|
||||
<div>
|
||||
{{character.alignment}} {{character.gender}} {{character.race}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex layout vertical end" style="margin-right: 2mm;">
|
||||
<div class="paper-font-body2 " style="font-size: 5mm !important;">
|
||||
dicecloud.com
|
||||
</div>
|
||||
<div>
|
||||
{{characterUrl}}
|
||||
</div>
|
||||
</div>
|
||||
<canvas id="qrCode"></canvas>
|
||||
</div>
|
||||
<div class="columns layout horizontal flex">
|
||||
<div class="col1 flex layout vertical">
|
||||
<div class="layout vertical center-justified" style="min-height: 100px; margin-bottom: 4mm;">
|
||||
<div class="initiative" style="margin-bottom: 2mm;">
|
||||
{{> printedLongStat stat="" name="Inspiration" prefix=""}}
|
||||
</div>
|
||||
<div class="proficiencyBonus">
|
||||
{{> printedLongStat stat="proficiencyBonus" name="Proficiency Bonus" prefix="+"}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout horizontal">
|
||||
<div class="abilities layout vertical justified" style="margin-right: 4mm;">
|
||||
{{> printedAbility ability="strength" title="Strength"}}
|
||||
{{> printedAbility ability="dexterity" title="Dexterity"}}
|
||||
{{> printedAbility ability="constitution" title="Constitution"}}
|
||||
{{> printedAbility ability="intelligence" title="Intelligence"}}
|
||||
{{> printedAbility ability="wisdom" title="Wisdom"}}
|
||||
{{> printedAbility ability="charisma" title="Charisma"}}
|
||||
</div>
|
||||
<div class="flex layout vertical">
|
||||
<div class="saves double-border" style="margin-bottom: 2mm">
|
||||
<div>
|
||||
{{> printedSkillRow name="Strength" skill="strengthSave"}}
|
||||
{{> printedSkillRow name="Dexterity" skill="dexteritySave"}}
|
||||
{{> printedSkillRow name="Constitution" skill="constitutionSave"}}
|
||||
{{> printedSkillRow name="Intelligence" skill="intelligenceSave"}}
|
||||
{{> printedSkillRow name="Wisdom" skill="wisdomSave"}}
|
||||
{{> printedSkillRow name="Charisma" skill="charismaSave"}}
|
||||
</div>
|
||||
<div class="paper-font-subhead layout vertical center">
|
||||
Saving Throws
|
||||
</div>
|
||||
</div>
|
||||
<div class="skills double-border">
|
||||
<div>
|
||||
{{> printedSkillRow name="Acrobatics" skill="acrobatics"}}
|
||||
{{> printedSkillRow name="Animal Handling" skill="animalHandling"}}
|
||||
{{> printedSkillRow name="Arcana" skill="arcana"}}
|
||||
{{> printedSkillRow name="Athletics" skill="athletics"}}
|
||||
{{> printedSkillRow name="Deception" skill="deception"}}
|
||||
{{> printedSkillRow name="History" skill="history"}}
|
||||
{{> printedSkillRow name="Insight" skill="insight"}}
|
||||
{{> printedSkillRow name="Intimidation" skill="intimidation"}}
|
||||
{{> printedSkillRow name="Investigation" skill="investigation"}}
|
||||
{{> printedSkillRow name="Medicine" skill="medicine"}}
|
||||
{{> printedSkillRow name="Nature" skill="nature"}}
|
||||
{{> printedSkillRow name="Perception" skill="perception" showPassive="true"}}
|
||||
{{> printedSkillRow name="Performance" skill="performance"}}
|
||||
{{> printedSkillRow name="Persuasion" skill="persuasion"}}
|
||||
{{> printedSkillRow name="Religion" skill="religion"}}
|
||||
{{> printedSkillRow name="Sleight of Hand" skill="sleightOfHand"}}
|
||||
{{> printedSkillRow name="Stealth" skill="stealth"}}
|
||||
{{> printedSkillRow name="Survival" skill="survival"}}
|
||||
</div>
|
||||
<div class="paper-font-subhead layout vertical center">
|
||||
Skills
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="proficiencies flex double-border" style="margin-top: 4mm">
|
||||
<div class="paper-font-subhead layout vertical center" style="margin-bottom: 2mm;">
|
||||
Proficiencies
|
||||
</div>
|
||||
<div class="layout horizontal">
|
||||
<div class="flex" style="margin-right: 2mm">
|
||||
{{#if armorProfs.length}}
|
||||
<div class="paper-font-subhead" style="margin-bottom: 1mm;">Armor</div>
|
||||
{{/if}}
|
||||
{{#each armorProfs}}
|
||||
{{> printedProficiency}}
|
||||
{{/each}}
|
||||
{{#if weaponProfs.length}}
|
||||
<div class="paper-font-subhead" style="margin: 2mm 0 1mm;">Weapons</div>
|
||||
{{/if}}
|
||||
{{#each weaponProfs}}
|
||||
{{> printedProficiency}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{#if toolProfs.length}}
|
||||
<div class="flex">
|
||||
<div class="paper-font-subhead" style="margin-bottom: 1mm;">Tools</div>
|
||||
{{#each toolProfs}}
|
||||
{{> printedProficiency}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col2 flex layout vertical" style="margin-left: 4mm; margin-right: 4mm;">
|
||||
<div class="layout horizontal center justified" style="min-height: 100px; margin-bottom: 4mm;">
|
||||
<div class="armor">
|
||||
{{> printedSquareStat stat="armor" name="Armor Class" class="shield-background"}}
|
||||
</div>
|
||||
<div class="inititive">
|
||||
{{> printedSquareStat stat="initiative" name="Initiative" isSkill="true" class="double-border"}}
|
||||
</div>
|
||||
<div class="speed">
|
||||
{{> printedSquareStat stat="speed" name="Speed" class="double-border"}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="hitpoints layout vertical double-border" style="margin-bottom: 2mm;">
|
||||
<div>
|
||||
Hit point maximum:
|
||||
<span class="paper-font-subhead">
|
||||
{{characterCalculate "attributeBase" _id "hitPoints"}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex" style="width: 3cm; height: 2cm;">
|
||||
<!-- Space for writing -->
|
||||
</div>
|
||||
<div class="layout vertical center paper-font-subhead">
|
||||
Hit Points
|
||||
</div>
|
||||
</div>
|
||||
<div class="tempHitpoints layout vertical double-border" style="margin-bottom: 2mm;">
|
||||
<div style="width: 3cm; height: 1.5cm;">
|
||||
<!-- Space for writing -->
|
||||
</div>
|
||||
<div class="layout vertical center paper-font-subhead">
|
||||
Temporary Hit Points
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout horizontal" style="margin-bottom: 4mm;">
|
||||
<div class="hitDice double-border flex layout vertical" style="margin-right: 2mm;">
|
||||
<div>
|
||||
Total:
|
||||
<span class="paper-font-subhead" style="text-transform: none !important;">
|
||||
{{hitDiceTotal}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex" style="min-height: 1cm;">
|
||||
<!-- Space for writing -->
|
||||
</div>
|
||||
<div class="paper-font-subhead layout vertical center">
|
||||
Hit Dice
|
||||
</div>
|
||||
</div>
|
||||
<div class="deathSaves layout vertical center double-border">
|
||||
<div class="" style="margin-bottom: 1mm;">
|
||||
Successes
|
||||
</div>
|
||||
<div class="layout horizontal center">
|
||||
<iron-icon icon="radio-button-unchecked"></iron-icon>
|
||||
<iron-icon icon="radio-button-unchecked"></iron-icon>
|
||||
<iron-icon icon="radio-button-unchecked"></iron-icon>
|
||||
</div>
|
||||
<div class="" style="margin: 1mm 0;">
|
||||
Failures
|
||||
</div>
|
||||
<div class="layout horizontal center">
|
||||
<iron-icon icon="radio-button-unchecked"></iron-icon>
|
||||
<iron-icon icon="radio-button-unchecked"></iron-icon>
|
||||
<iron-icon icon="radio-button-unchecked"></iron-icon>
|
||||
</div>
|
||||
<div class="paper-font-subhead layout vertical center" style="margin-top: 2mm;">
|
||||
Death Saves
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="attacks double-border flex">
|
||||
<div class="paper-font-subhead layout vertical center" style="margin-bottom: 2mm;">
|
||||
Attacks
|
||||
</div>
|
||||
{{#each attack in attacks}}
|
||||
{{> printedAttack attack=attack charId=_id}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col3 flex layout vertical">
|
||||
<div class="Languages double-border" style="min-height: 100px; margin-bottom: 4mm;">
|
||||
<div class="paper-font-subhead layout vertical center" style="margin-bottom: 2mm;">
|
||||
Languages
|
||||
</div>
|
||||
<div class="layout horizontal">
|
||||
<div class="flex" style="margin-right: 2mm;">
|
||||
{{#each languageProfs.left}}
|
||||
{{> printedProficiency}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{#if languageProfs.right.length}}
|
||||
<div class="flex">
|
||||
{{#each languageProfs.right}}
|
||||
{{> printedProficiency}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="traits double-border">
|
||||
{{#markdown}}{{evaluateShortString character._id character.personality}}{{/markdown}}
|
||||
<div class="paper-font-subhead layout vertical center">
|
||||
Personality traits
|
||||
</div>
|
||||
</div>
|
||||
<div class="ideals double-border" style="margin-top: 2mm">
|
||||
{{#markdown}}{{evaluateShortString character._id character.ideals}}{{/markdown}}
|
||||
<div class="paper-font-subhead layout vertical center">
|
||||
Ideals
|
||||
</div>
|
||||
</div>
|
||||
<div class="bonds double-border" style="margin-top: 2mm">
|
||||
{{#markdown}}{{evaluateShortString character._id character.bonds}}{{/markdown}}
|
||||
<div class="paper-font-subhead layout vertical center">
|
||||
Bonds
|
||||
</div>
|
||||
</div>
|
||||
<div class="flaws double-border" style="margin-top: 2mm">
|
||||
{{#markdown}}{{evaluateShortString character._id character.flaws}}{{/markdown}}
|
||||
<div class="paper-font-subhead layout vertical center">
|
||||
Flaws
|
||||
</div>
|
||||
</div>
|
||||
<div class="background double-border flex layout vertical" style="margin-top: 2mm">
|
||||
<div class="paper-font-subhead layout vertical center" style="margin-bottom: 4mm">
|
||||
Notes
|
||||
</div>
|
||||
<div class="flex lined-background">
|
||||
<!-- lined space for writing -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,80 @@
|
||||
import QRCode from "qrcode"
|
||||
|
||||
Template.printedCharacterSheet.onRendered(function(){
|
||||
// Quickfit is only called once on rendering, text will not resize reactively
|
||||
this.$(".shrink-to-fit").quickfit({
|
||||
min: 7,
|
||||
max: 36,
|
||||
truncate: true,
|
||||
});
|
||||
let url = `https://dicecloud.com/character/${this.data._id}`;
|
||||
let canvas = this.find("#qrCode");
|
||||
QRCode.toCanvas(canvas, url, {
|
||||
margin: 0,
|
||||
width: 200,
|
||||
}, function(error){
|
||||
$(canvas).css("width", "60px").css("height", "60px");
|
||||
if (error) console.error(error)
|
||||
});
|
||||
});
|
||||
|
||||
Template.printedCharacterSheet.helpers({
|
||||
character(){
|
||||
return Characters.findOne(this._id);
|
||||
},
|
||||
classes: function(){
|
||||
return Classes.find({charId: this._id}, {sort: {createdAt: 1}});
|
||||
},
|
||||
weaponProfs: function(){
|
||||
var profs = Proficiencies.find({charId: this._id, type: "weapon"});
|
||||
return removeDuplicateProficiencies(profs);
|
||||
},
|
||||
armorProfs: function(){
|
||||
var profs = Proficiencies.find({charId: this._id, type: "armor"});
|
||||
return removeDuplicateProficiencies(profs);
|
||||
},
|
||||
toolProfs: function(){
|
||||
var profs = Proficiencies.find({charId: this._id, type: "tool"});
|
||||
return removeDuplicateProficiencies(profs);
|
||||
},
|
||||
languageProfs: function(){
|
||||
var profs = Proficiencies.find({charId: this._id, type: "language"});
|
||||
profs = removeDuplicateProficiencies(profs);
|
||||
if (profs.length > 3){
|
||||
var halfway = Math.floor(profs.length / 2);
|
||||
var left = profs.slice(0, halfway);
|
||||
var right = profs.slice(halfway);
|
||||
return {left, right};
|
||||
} else {
|
||||
return {left: profs, right: []};
|
||||
}
|
||||
},
|
||||
attacks: function(){
|
||||
return Attacks.find(
|
||||
{charId: this._id, enabled: true},
|
||||
{sort: {color: 1, name: 1}});
|
||||
},
|
||||
hitDiceTotal: function(){
|
||||
let d6 = Characters.calculate.attributeValue(this._id, "d6HitDice");
|
||||
let d8 = Characters.calculate.attributeValue(this._id, "d8HitDice");
|
||||
let d10 = Characters.calculate.attributeValue(this._id, "d10HitDice");
|
||||
let d12 = Characters.calculate.attributeValue(this._id, "d12HitDice");
|
||||
d6 = d6 ? d6 + "d6" : "";
|
||||
d8 = d8 ? d8 + "d8" : "";
|
||||
d10 = d10 ? d10 + "d10" : "";
|
||||
d12 = d12 ? d12 + "d12" : "";
|
||||
return [d6, d8, d10, d12].filter(Boolean).join(" ");
|
||||
},
|
||||
characterUrl: function(){
|
||||
return `/character/${this._id}`
|
||||
},
|
||||
});
|
||||
|
||||
Template.printedCharacterSheet.events({
|
||||
"click .printButton": function(event, instance){
|
||||
print();
|
||||
},
|
||||
"click .backButton": function(event, instance){
|
||||
history && history.back();
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
.printedLongStat .title {
|
||||
white-space: nowrap;
|
||||
margin-left: 2mm;
|
||||
}
|
||||
|
||||
.printedLongStat .numbers {
|
||||
z-index: 1;
|
||||
min-width: 74px;
|
||||
min-height: 45px;
|
||||
}
|
||||
|
||||
.printed .printedLongStat.double-border{
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.printed .printedLongStat.double-border:before {
|
||||
top: 4px;
|
||||
bottom: 4px;
|
||||
left: 33px;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<template name="printedLongStat">
|
||||
<div class="printedLongStat layout horizontal double-border">
|
||||
<div class="numbers paper-font-display1 octogon-border">
|
||||
{{#if stat}}
|
||||
{{#if isSkill}}
|
||||
{{prefix}}{{skillMod}}
|
||||
{{else}}
|
||||
{{prefix}}{{characterCalculate "attributeValue" ../_id stat}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="paper-font-subhead title flex layout horizontal center">
|
||||
{{name}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
Template.printedLongStat.helpers({
|
||||
skillMod: function() {
|
||||
return signedString(
|
||||
Characters.calculate.skillMod(
|
||||
Template.parentData()._id, this.stat
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
.printedProficiency iron-icon {
|
||||
margin-right: 2mm;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<template name="printedProficiency">
|
||||
<div class="printedProficiency layout horizontal center">
|
||||
<iron-icon icon="{{profIcon}}"></iron-icon>
|
||||
<div>{{getName}}</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
Template.printedProficiency.helpers({
|
||||
profIcon: function(){
|
||||
var prof = this.value;
|
||||
if (prof > 0 && prof < 1) return "image:brightness-2";
|
||||
if (prof === 1) return "image:brightness-1";
|
||||
if (prof > 1) return "av:album";
|
||||
return "radio-button-off";
|
||||
},
|
||||
getName: function(){
|
||||
if (this.type === "skill") return skills[this.name];
|
||||
if (this.type === "save") return saves[this.name];
|
||||
return this.name;
|
||||
},
|
||||
});
|
||||
|
||||
Template.printedProficiency.events({
|
||||
"click .proficiency": function(event, instance){
|
||||
if (this.parent.collection == "Characters") {
|
||||
if (this.parent.group == "background") {
|
||||
pushDialogStack({
|
||||
template: "backgroundDialog",
|
||||
data: {
|
||||
"charId": this.charId,
|
||||
"field":"background",
|
||||
"title":"Background",
|
||||
"color":"j",
|
||||
},
|
||||
element: event.currentTarget,
|
||||
})
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
openParentDialog({
|
||||
parent: this.parent,
|
||||
charId: this.charId,
|
||||
element: event.currentTarget,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
.printedSkillRow {
|
||||
height: 24px;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.printedSkillRow .skill-mod {
|
||||
width: 36px;
|
||||
text-align: center;
|
||||
font-size: 3.5mm;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<template name="printedSkillRow">
|
||||
<div class="printedSkillRow layout horizontal center">
|
||||
<iron-icon icon="{{profIcon}}"></iron-icon>
|
||||
{{#if failSkill}}
|
||||
<div class="fail skill-mod">fail</div>
|
||||
{{else}}
|
||||
<div class="{{advantage}} skill-mod">
|
||||
{{skillMod}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<div flex>
|
||||
{{name}}
|
||||
{{#if conditionalCount}}
|
||||
*
|
||||
{{/if}}
|
||||
{{#if showPassive}}
|
||||
({{characterCalculate "passiveSkill" ../_id skill}})
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,41 @@
|
||||
Template.printedSkillRow.helpers({
|
||||
skillMod: function() {
|
||||
return signedString(
|
||||
Characters.calculate.skillMod(
|
||||
Template.parentData()._id, this.skill
|
||||
)
|
||||
);
|
||||
},
|
||||
profIcon: function(){
|
||||
var charId = Template.parentData()._id;
|
||||
var prof = Characters.calculate.proficiency(charId, this.skill);
|
||||
if (prof > 0 && prof < 1) return "image:brightness-2";
|
||||
if (prof === 1) return "image:brightness-1";
|
||||
if (prof > 1) return "av:album";
|
||||
return "radio-button-unchecked";
|
||||
},
|
||||
failSkill: function(){
|
||||
var charId = Template.parentData()._id;
|
||||
return Effects.find({
|
||||
charId: charId,
|
||||
stat: this.skill,
|
||||
enabled: true,
|
||||
operation: "fail",
|
||||
}).count();
|
||||
},
|
||||
advantage: function(){
|
||||
var charId = Template.parentData()._id;
|
||||
var advantage = Characters.calculate.advantage(charId, this.skill);
|
||||
if (advantage > 0) return "advantage";
|
||||
if (advantage < 0) return "disadvantage";
|
||||
},
|
||||
conditionalCount: function(){
|
||||
var charId = Template.parentData()._id;
|
||||
return Effects.find({
|
||||
charId: charId,
|
||||
stat: this.skill,
|
||||
enabled: true,
|
||||
operation: "conditional",
|
||||
}).count();
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
.printedSquareStat {
|
||||
min-width: 67px;
|
||||
}
|
||||
|
||||
.printedSquareStat .title.paper-font-subhead {
|
||||
font-size: 2.5mm !important;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<template name="printedSquareStat">
|
||||
<div class="printedSquareStat layout vertical center {{class}}">
|
||||
<div class="numbers paper-font-display1">
|
||||
{{#if isSkill}}
|
||||
{{prefix}}{{skillMod}}
|
||||
{{else}}
|
||||
{{prefix}}{{characterCalculate "attributeValue" ../_id stat}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="paper-font-subhead title">
|
||||
{{name}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
Template.printedSquareStat.helpers({
|
||||
skillMod: function() {
|
||||
return signedString(
|
||||
Characters.calculate.skillMod(
|
||||
Template.parentData()._id, this.stat
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -279,23 +279,23 @@ Template.spells.events({
|
||||
collection: "SpellLists",
|
||||
};
|
||||
spell.prepared = "prepared";
|
||||
Spells.insert(spell);
|
||||
let insertedSpellId = Spells.insert(spell);
|
||||
// Copy over attacks and effects
|
||||
_.each(rawSpell.attacks, (attack) => {
|
||||
if (!("attackBonus" in attack)) {attack.attackBonus = "attackBonus"} //if no attack bonus provided, use spell list's
|
||||
attack.charId = charId;
|
||||
attack.parent = {id: spellId, collection: "Spells"};
|
||||
attack.parent = {id: insertedSpellId, collection: "Spells"};
|
||||
Attacks.insert(attack);
|
||||
});
|
||||
_.each(rawSpell.effects, (effect) => {
|
||||
effect.charId = charId;
|
||||
effect.parent = {id: spellId, collection: "Spells"};
|
||||
effect.parent = {id: insertedSpellId, collection: "Spells"};
|
||||
Effects.insert(effect);
|
||||
});
|
||||
|
||||
_.each(rawSpell.buffs, (buff) => {
|
||||
buff.charId = charId;
|
||||
buff.parent = {id: spellId, collection: "Spells"};
|
||||
buff.parent = {id: insertedSpellId, collection: "Spells"};
|
||||
buffId = Buffs.insert(buff);
|
||||
|
||||
_.each(buff.attacks, (attack) => {
|
||||
|
||||
@@ -23,4 +23,17 @@
|
||||
<meta name="msapplication-TileColor" content="#b91d1d">
|
||||
<meta name="msapplication-TileImage" content="/mstile-144x144.png?v=lk6WXp6Pmj">
|
||||
<meta name="theme-color" content="#d12929">
|
||||
|
||||
<style type="text/css" media="print">
|
||||
@page {
|
||||
margin: 0mm;
|
||||
}
|
||||
html {
|
||||
margin: 0px;
|
||||
}
|
||||
* {
|
||||
-webkit-transition: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
20
rpg-docs/client/views/meta/error/error.html
Normal file
20
rpg-docs/client/views/meta/error/error.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<template name="error">
|
||||
<app-header-layout has-scrolling-region fullbleed>
|
||||
<app-header class="app-grey white-text" fixed>
|
||||
<app-toolbar>
|
||||
<paper-icon-button icon="menu" drawer-toggle></paper-icon-button>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
<div class="fit layout vertical center center-justified">
|
||||
<div class="paper-font-subhead"
|
||||
style="margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
text-align: center;">
|
||||
{{#if errorMessage}}
|
||||
<div>{{errorMessage}}</div>
|
||||
<paper-button class="try-again">Try Again</paper-button>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</app-header-layout>
|
||||
</template>
|
||||
17
rpg-docs/client/views/meta/error/error.js
Normal file
17
rpg-docs/client/views/meta/error/error.js
Normal file
@@ -0,0 +1,17 @@
|
||||
Template.error.onRendered(function(){
|
||||
const error = Session.get("error") || {};
|
||||
if (error.href) window.history.replaceState("", "", error.href);
|
||||
});
|
||||
|
||||
Template.error.helpers({
|
||||
errorMessage: function(){
|
||||
const error = Session.get("error") || {};
|
||||
return error.reason;
|
||||
},
|
||||
});
|
||||
|
||||
Template.error.events({
|
||||
"click .try-again": function(event, instance){
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
@@ -68,7 +68,7 @@
|
||||
transform-origin: top left;
|
||||
transition: top 400ms ease, left 400ms ease;
|
||||
z-index: 3;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.dialog-stack .dialog .testButton {
|
||||
|
||||
@@ -42,6 +42,26 @@
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
API Key
|
||||
</td>
|
||||
<td class="apiKey">
|
||||
{{#if apiKey}}
|
||||
{{#unless showApiKey}}
|
||||
<paper-button class="showApiKey">
|
||||
Show
|
||||
</paper-button>
|
||||
{{else}}
|
||||
{{apiKey}}
|
||||
{{/unless}}
|
||||
{{else}}
|
||||
<paper-button class="generateMyApiKey">
|
||||
Generate
|
||||
</paper-button>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div style="max-width: 250px">
|
||||
{{> atForm state="signIn"}}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
Template.profile.onCreated(function(){
|
||||
this.showApiKey = new ReactiveVar(false);
|
||||
});
|
||||
|
||||
Template.profile.helpers({
|
||||
profileName: function() {
|
||||
var user = Meteor.user();
|
||||
return user.profile && user.profile.username ||
|
||||
user.username ||
|
||||
"Tap to set username";
|
||||
}
|
||||
},
|
||||
showApiKey: function(){
|
||||
return Template.instance().showApiKey.get();
|
||||
},
|
||||
});
|
||||
|
||||
Template.profile.events({
|
||||
@@ -25,4 +32,11 @@ Template.profile.events({
|
||||
data: {},
|
||||
});
|
||||
},
|
||||
"click .showApiKey": function(event, instance){
|
||||
instance.showApiKey.set(!instance.showApiKey.get());
|
||||
},
|
||||
"click .generateMyApiKey": function(event, instance){
|
||||
Meteor.call("generateMyApiKey");
|
||||
instance.showApiKey.set(true);
|
||||
},
|
||||
});
|
||||
|
||||
1901
rpg-docs/package-lock.json
generated
Normal file
1901
rpg-docs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,10 @@
|
||||
"dependencies": {
|
||||
"@polymer/polymer": "^1.2.5-npm-test.2",
|
||||
"babel-runtime": "^6.23.0",
|
||||
"bcrypt": "^1.0.2",
|
||||
"bower": "^1.7.9"
|
||||
"bcrypt": "^1.0.3",
|
||||
"bower": "^1.7.9",
|
||||
"core-js": "^2.5.1",
|
||||
"meteor-node-stubs": "^0.3.2",
|
||||
"qrcode": "^1.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
rpg-docs/public/png/doubleLineImageBorder.png
Normal file
BIN
rpg-docs/public/png/doubleLineImageBorder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
BIN
rpg-docs/public/png/horizontalLine.png
Normal file
BIN
rpg-docs/public/png/horizontalLine.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 180 B |
BIN
rpg-docs/public/png/octogonBorder.png
Normal file
BIN
rpg-docs/public/png/octogonBorder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
rpg-docs/public/png/shieldBorder.png
Normal file
BIN
rpg-docs/public/png/shieldBorder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
rpg-docs/public/png/upwardPointingBorder.png
Normal file
BIN
rpg-docs/public/png/upwardPointingBorder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
8
rpg-docs/server/lib/logRateError.js
Normal file
8
rpg-docs/server/lib/logRateError.js
Normal file
@@ -0,0 +1,8 @@
|
||||
logRateError = function(reply, ruleInput){
|
||||
// reply = {allowed, timeToReset, numInvocationsLeft}
|
||||
// ruleInput = {userId, clientAddress, type, name, connectionId}
|
||||
console.log(
|
||||
`Limit hit for ${ruleInput.type} "${ruleInput.name}" ` +
|
||||
`by user ${ruleInput.userId} from ${ruleInput.clientAddress}`
|
||||
);
|
||||
}
|
||||
@@ -38,9 +38,13 @@ Meteor.publish("singleCharacter", function(characterId){
|
||||
DDPRateLimiter.addRule({
|
||||
name: "singleCharacter",
|
||||
type: "subscription",
|
||||
userId(){ return true; },
|
||||
userId: null,
|
||||
connectionId(){ return true; },
|
||||
}, 8, 5000);
|
||||
}, 8, 10000, function(reply, ruleInput){
|
||||
if(!reply.allowed){
|
||||
logRateError(reply, ruleInput);
|
||||
}
|
||||
});
|
||||
|
||||
Meteor.publish("singleCharacterName", function(characterId){
|
||||
userId = this.userId;
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
Meteor.publish("user", function(){
|
||||
return Meteor.users.find(this.userId, {fields: {roles: 1}});
|
||||
return Meteor.users.find(this.userId, {fields: {
|
||||
roles: 1,
|
||||
username: 1,
|
||||
profile: 1,
|
||||
apiKey: 1,
|
||||
}});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user